<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>R-bloggers</title>
	<atom:link href="https://www.r-bloggers.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://www.r-bloggers.com</link>
	<description>R news and tutorials contributed by hundreds of R bloggers</description>
	<lastBuildDate>Tue, 02 Jun 2026 22:00:00 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=5.5.18</generator>

<image>
	<url>https://i0.wp.com/www.r-bloggers.com/wp-content/uploads/2016/08/cropped-R_single_01-200.png?fit=32%2C32&#038;ssl=1</url>
	<title>R-bloggers</title>
	<link>https://www.r-bloggers.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">11524731</site>	<item>
		<title>Football meets machine learning: Forecasting the 2026 FIFA World Cup</title>
		<link>https://www.r-bloggers.com/2026/06/football-meets-machine-learning-forecasting-the-2026-fifa-world-cup/</link>
		
		<dc:creator><![CDATA[Achim Zeileis]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.zeileis.org/news/fifa2026/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>    Probabilistic forecasts for the 2026 FIFA World Cup are obtained by using a hybrid model that combines data, expert insights, and advanced statistical models. The favorite is Spain, closely followed by England, France, and Germany.</p>
<p>...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/football-meets-machine-learning-forecasting-the-2026-fifa-world-cup/">Football meets machine learning: Forecasting the 2026 FIFA World Cup</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.zeileis.org/news/fifa2026/"> Achim Zeileis</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

    
    <p>Probabilistic forecasts for the 2026 FIFA World Cup are obtained by using a hybrid model that combines data, expert insights, and advanced statistical models. The favorite is Spain, closely followed by England, France, and Germany.</p>
    
    
    <div class="row t20 b20">
  <div class="small-8 medium-9 large-10 columns">
  Football fans around the world are looking forward to the kick-off of the 2026 FIFA World Cup in Canada, Mexico, and the United States next week. 48 of the best teams from all around the world will compete from 11 June to 19 July to determine the new World Champion. In anticipation of the tournament the big question is who among the teams will succeed, who will drop out, and who will eventually prevail. While it is, of course, not yet possible to give <em>definitive</em> answers to these questions, we are able to provide <em>probabilistic</em> forecasts for all possible matches using a refined machine learning algorithm. This allows us to explore the likely course of the tournament by simulation.
  </div>
  <div class="small-4 medium-3 large-2 columns">
  <a href="https://www.fifa.com/worldcup/fifaworldcup2026/" alt="2026 FIFA World Cup web page" rel="nofollow" target="_blank"><img src="https://i2.wp.com/upload.wikimedia.org/wikipedia/en/thumb/1/17/2026_FIFA_World_Cup_emblem.svg/250px-2026_FIFA_World_Cup_emblem.svg.png?w=578&#038;ssl=1" alt="2026 FIFA World Cup logo" data-recalc-dims="1" /></a>
  </div>
</div>

<h2 id="winning-probabilities">Winning probabilities</h2>

<p>The forecast is based on a machine learning algorithm that blends a variety of different sources of information: An ability estimate for every team based on historic matches; an ability estimate for every team based on odds from 24 bookmakers; average ratings of the players in each team based on their individual performances in their home clubs and national teams; the average market value of all players in each team according to a wisdom-of-the-crowd approach; further team and country covariates (e.g., FIFA and Elo ratings or GDP). A machine learning algorithm is trained on the results of all major football tournaments (Men’s World Cups and Euros) between 2006 and 2024 and then applied to current information to obtain a forecast for the 2026 FIFA World Cup. More specifically, the algorithm estimates the predicted number of goals for all possible matches between all 48 teams in the tournament. Based on the predicted goals the probabilities for each potential outcome (i.e., 0-0, 1-0, 0-1, 2-0, etc.) in each of these matches can be computed from a bivariate Poisson distribution (here: assuming independence). This allows us to simulate all matches in the group phase and which teams proceed to the knockout stage and who eventually wins. Repeating the simulation 100,000 times yields winning probabilities for each team. The results show that Spain is the favorite for the title with a winning probability of 14.5%, closely followed by England and France, both with 12.4%, and Germany with 11.2%. The winning probabilities for all teams are shown in the barchart below with more information linked in the interactive full-width version.</p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_win.html" rel="nofollow" target="_blank">Interactive full-width graphic</a></p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_win.html" rel="nofollow" target="_blank"><img src="https://i1.wp.com/www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_win.png?w=578&#038;ssl=1" alt="Barchart: Winning probabilities" data-recalc-dims="1" /></a></p>

<p>The study has been conducted by an international team of researchers: <a href="https://bd.statistik.tu-dortmund.de/professur/arbeitsgruppe/prof-dr-andreas-groll/" rel="nofollow" target="_blank">Andreas Groll</a>, Agamyrat Hanekov, <a href="https://home.himolde.no/hvattum/" rel="nofollow" target="_blank">Lars Magnus Hvattum</a>, <a href="https://rouvenm.github.io/" rel="nofollow" target="_blank">Rouven Michels</a>, <a href="https://www.sg.tum.de/epidemiologie/team/schauberger/" rel="nofollow" target="_blank">Gunther Schauberger</a>, <a href="https://de.linkedin.com/in/elinasukhanova" rel="nofollow" target="_blank">Elina Sukhanova</a>, Sebastian Witte, <a href="https://www.zeileis.org/" rel="nofollow" target="_blank">Achim Zeileis</a>.
The basic idea for the forecast is to proceed in two steps. In the first step, sophisticated statistical models as well as expert insights are employed to determine the strengths of all teams and their players using disparate sets of information. In the second step, a machine learning algorithm decides how to best combine the strength estimates with other information about the teams.</p>

<ul>
  <li>
    <p><em>Historic information: Match abilities.</em><br />
An ability estimate is obtained for every team based on “retrospective” data, namely all historic national matches over the last 8 years (freely curated by <a href="https://www.kaggle.com/datasets/martj42/international-football-results-from-1872-to-2017" rel="nofollow" target="_blank">Mart Jürisoo on Kaggle</a>). A <em>bivariate Poisson model</em> with team-specific fixed effects and assuming independence is fitted to the number of goals scored by both teams in each match. However, rather than equally weighting all matches to obtain <em>average</em> team abilities (or team strengths) over the entire history period, an exponential weighting scheme is employed. This assigns more weight to more recent results and thus yields an estimate of <em>current</em> team abilities. More details can be found in <a href="https://doi.org/10.1177%2F1471082X18817650" rel="nofollow" target="_blank">Ley, Van de Wiele, Van Eetvelde (2019)</a>.</p>
  </li>
  <li>
    <p><em>Future expectation: Bookmaker consensus abilities.</em><br />
Another ability estimate for every team is obtained based on “prospective” data, namely the odds of 24 international bookmakers that reflect their expert expectations for the tournament. Using the <em>bookmaker consensus model</em> of <a href="https://dx.doi.org/10.1016/j.ijforecast.2009.10.001" rel="nofollow" target="_blank">Leitner, Zeileis, Hornik (2010)</a>, the bookmaker odds are first adjusted for the bookmakers’ profit margins (“overround”) and then averaged (on a logit scale) to obtain a consensus for the winning probability of each team. To adjust for the effects of the tournament draw (that might have led to easier or harder groups for some teams), an “inverse” simulation approach is used to infer which team abilities are most likely to lead up to the consensus winning probabilities.</p>
  </li>
  <li>
    <p><em>Individual player contributions: Average player ratings.</em><br />
To infer the “contributions of individual players” in a match, the <em>plus-minus player ratings</em> of <a href="https://doi.org/10.1007/s11750-020-00584-9" rel="nofollow" target="_blank">Pantuso &#038; Hvattum (2021)</a> dissect all matches with a certain player (both on club and on national level) into segments, e.g., between substitutions. Subsequently, the goal difference achieved in these segments is linked to the presence of the individual players during that segment. This yields individual ratings for all players that can be aggregated to average player ratings for each team.</p>
  </li>
  <li>
    <p><em>Wisdom of the crowd: Average market values:</em><br />
Another way to reflect the current quality and the future potential of each player in a team is to consider their expected market value. As the real market values are unknown, the <a href="https://www.transfermarkt.co.uk/" rel="nofollow" target="_blank">Transfermarkt</a> web portal employs a “wisdom-of-the-crowd” approach to determine current expected market values for all players. These are based on discussions relying on publicly available data among the online community members of the portal and moderated and consolidated by expert community members and the portal’s employees.</p>
  </li>
  <li>
    <p><em>Combination with present status: Hybrid random forests.</em><br />
Finally, machine learning is used to combine these four highly aggregated and informative variables with a broad range of further relevant covariates reflecting the current states of the different teams and the countries they come from. Such a hybrid approach was first suggested by <a href="https://doi.org/10.1515/jqas-2018-0060" rel="nofollow" target="_blank">Groll, Ley, Schauberger, Van Eetvelde (2019)</a>. A random forest algorithm is trained to decide how to blend the different ability estimates with team-specific features that are typically less informative but still powerful enough to enhance the forecasts. The features considered comprise team-specific details (e.g., FIFA rank, Elo rating, number of Champions League players) as well as country-specifc socio-economic factors (such as GDP per capita). By combining a large ensemble of rather weakly informative regression trees in a random forest, the relative importances of all the covariates can be inferred automatically. The resulting predicted number of goals for each team can then finally be used to simulate the entire tournament 100,000 times.</p>
  </li>
</ul>

<h2 id="match-probabilities">Match probabilities</h2>

<p>Using the forecasts from the machine learning algorithm yields the predicted number of goals for both teams in each possible match. The explanatory information used for this is the difference between the two teams in each of the variables listed above, i.e., the difference in historic match abilities (on a log scale), the difference in bookmaker consensus abilities (on a log scale), difference in average player ratings of the teams, difference in log market values, etc. The predicted number of goals for the two teams in each match can then be plugged as expectations into two independent Poisson distributions, from which we can compute the probability that a certain match ends in a <em>win</em>, a <em>draw</em>, or a <em>loss</em>. The same can be repeated in overtime, if necessary, and a coin flip is used to decide penalties, if needed.</p>

<p>The following heatmap shows for each possible combination of teams the probability that one team beats the other team in a knockout match. The color scheme uses green vs. purple to signal probabilities above vs. below 50%, respectively. The tooltips for each match in the interactive version of the graphic also print the probabilities for the match to end in a <em>win</em>, <em>draw</em>, or <em>loss</em> after normal time.</p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_match.html" rel="nofollow" target="_blank">Interactive full-width graphic</a></p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_match.html" rel="nofollow" target="_blank"><img src="https://i0.wp.com/www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_match.png?w=578&#038;ssl=1" alt="Heatmap: Match probabilities" data-recalc-dims="1" /></a></p>

<h2 id="performance-throughout-the-tournament">Performance throughout the tournament</h2>

<p>As the goals for both teams in every single match can be simulated with the approach described above, it is also straightfoward to simulate the entire tournament (here: 100,000 times) providing “survival” probabilities for each team across the different stages.</p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_surv.html" rel="nofollow" target="_blank">Interactive full-width graphic</a></p>

<p><a href="https://www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_surv.html" rel="nofollow" target="_blank"><img src="https://i1.wp.com/www.zeileis.org/assets/posts/2026-06-03-fifa2026/p_surv.png?w=578&#038;ssl=1" alt="Line plot: Survival probabilities" data-recalc-dims="1" /></a></p>

<h2 id="odds-and-ends">Odds and ends</h2>

<p>All our forecasts are probabilistic, clearly below 100%, and hence by no means certain. Although we can quantify this uncertainty in terms of probabilities from a multiverse of potential tournaments, it is far from being predetermined which of these potential tournaments we will eventually see during the actual tournament.</p>

<p>Nevertheless the probabilistic view provides us with some interesting insights: For example, compared to predictions for previous tournaments (see e.g., <a href="https://www.zeileis.org/news/fifa2018/" rel="nofollow" target="_blank">2018</a>, <a href="https://www.zeileis.org/news/fifa2022/" rel="nofollow" target="_blank">2022</a>), it is even more uncertain who will win the title as there are a number of teams with good (albeit none with very high) chances of winning the tournament. An important factor for this is the substantially increased size of the tournament with 48 teams (rather than the previous 32) and an additional knockout round. Also, the tournament draw is much more variable, because 8 of the 12 third-ranked teams proceed to the knockout stage with 495 (!) possible permutations for mapping groups to matches in the round of 32.</p>

<p>Moreover, comparing our forecasts to those based only on the bookmakers odds, it is striking that Germany is ranked 4th, closely behind the three top teams, while it is only ranked 7th by many bookmakers. Conversely, Brazil and Argentina are typically ranked higher by the bookmakers but perform worse in our machine-learning-calibrated simulation.</p>

<p>In any case, all of this means that the probabilistic forecasts leave a lot of room for surprises and excitement during the 2026 FIFA World Cup. But what is absolutely certain is that we look forward to an entertaining tournament as football fans (much more than as professional forecasters).</p>

    
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.zeileis.org/news/fifa2026/"> Achim Zeileis</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/football-meets-machine-learning-forecasting-the-2026-fifa-world-cup/">Football meets machine learning: Forecasting the 2026 FIFA World Cup</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401648</post-id>	</item>
		<item>
		<title>A Multi-Agent DDQN Strategic Audit Engine for Silver Markets using Keras/Tensorflow</title>
		<link>https://www.r-bloggers.com/2026/06/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/</link>
		
		<dc:creator><![CDATA[Selcuk Disci]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 12:46:44 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://datageeek.com/?p=12123</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> 1. Introduction &#038; Theoretical Framework In modern electronic trading markets, algorithmic execution engines drive the vast majority of institutional order flows. Evaluating whether these independent, learning-driven trading algorithms behave competitively or tacitly coordinate has become a critical challenge for quantitative compliance, market microstructure design, and risk management. This technical article implements an ...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/">A Multi-Agent DDQN Strategic Audit Engine for Silver Markets using Keras/Tensorflow</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://datageeek.com/2026/06/02/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/"> DataGeeek</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<h2 class="wp-block-heading">1. Introduction &#038; Theoretical Framework</h2>



<p class="wp-block-paragraph">In modern electronic trading markets, algorithmic execution engines drive the vast majority of institutional order flows. Evaluating whether these independent, learning-driven trading algorithms behave competitively or tacitly coordinate has become a critical challenge for quantitative compliance, market microstructure design, and risk management.</p>



<p class="wp-block-paragraph">This technical article implements an automated <strong>Strategic Audit Engine</strong> designed to evaluate algorithmic execution regimes in the Silver futures market (<code>SI=F</code>). Our framework is explicitly built upon the empirical and theoretical foundations laid out by <strong>Koulouris &#038; Campajola (2026)</strong> in their groundbreaking paper, <em>“Memory-Induced Supra-Competitive Outcomes Between Deep Reinforcement Learning Agents in Optimal Trade Execution”</em> (<a href="https://arxiv.org/html/2605.20348v1" rel="nofollow" target="_blank"><strong><em>arXiv:2605.20348v1, May 2026</em></strong></a>).</p>



<h3 class="wp-block-heading">The Core Thesis: Supra-Competitive Outcomes via Memory Paths</h3>



<p class="wp-block-paragraph">Traditional regulatory frameworks look for <em>explicit collusion</em> (active communication or cartel setups). However, Koulouris &#038; Campajola demonstrate a far more subtle phenomenon: when independent Deep Reinforcement Learning (DRL) agents are equipped with memory—meaning they learn from rolling windows of historical price trajectories—they naturally converge toward <strong>supra-competitive outcomes</strong>. These are states where joint rewards remain artificially high, or execution parameters naturally align to mimic cooperation, without any explicit information exchange.</p>



<p class="wp-block-paragraph">To audit this behavior empirically, our engine models a symmetric duopoly market interaction. It maps the actual market execution path against two fundamental game-theoretic baselines:</p>



<ul class="wp-block-list">
<li><strong>The Cooperative Boundary (TWAP / Pareto Frontier):</strong> An idealized, optimal trade execution path where volume is distributed evenly across time to minimize joint market impact and maximize long-term mutual utility.</li>



<li><strong>The Competitive Boundary (Nash Equilibrium):</strong> The aggressive, non-cooperative state where individual agents structurally undercut each other, driving <strong>execution shortfall parameters</strong> to their maximum baseline.</li>
</ul>



<h2 class="wp-block-heading">2. Technical Stack &#038; Environmental Setup</h2>



<p class="wp-block-paragraph">To build a production-grade, reproducible multi-agent simulation pipeline, we leverage a hybrid data-science and deep-learning toolkit within the R ecosystem:</p>



<ul class="wp-block-list">
<li><strong><code>tidyquant</code> &#038; <code>tidyverse</code></strong><span style="margin: 0px;padding: 0px">: Serve as our core data engineering layer, managing financial API queries, formatting continuous return matrices, and handling functional list columns</span>.</li>



<li><strong><code>keras</code> &#038; <code>tensorflow</code>:</strong> Form the algorithmic backbone, allowing us to build, train, and run simultaneous forward/backward passes on Deep Q-Networks.</li>



<li><strong><code>ggtext</code> &#038; <code>glue</code>:</strong> Empower our visualization suite to parse inline HTML canvas rendering and handle dynamic string interpolations smoothly.</li>
</ul>


<pre>
# 1. ENVIRONMENT SETUP
if (!require(&quot;pacman&quot;)) install.packages(&quot;pacman&quot;)
pacman::p_load(tidyquant, tidyverse, ggtext, glue, keras, tensorflow)
</pre>


<h2 class="wp-block-heading">3. Building the Double Deep Q-Network Topology</h2>



<p class="wp-block-paragraph">Following the paper’s thesis on symmetric duopoly interactions, we construct two structurally identical execution agents: <code>agent_A</code> and <code>agent_B</code>. Both utilize a Dense Neural Network (Multilayer Perceptron) architecture to approximate the action-value space, denoted as <strong><em>Q(s, a)</em></strong>.</p>



<p class="wp-block-paragraph">The state space contains 3 features: <strong>Price Deviation</strong>, <strong>Asset Volatility (sigma)</strong>, and <strong>Relative Time Horizon</strong>. The output layer projects to 3 discrete strategic action coordinates via a linear activation function.</p>


<pre>
# 2. SYMMETRIC AGENT ARCHITECTURE
build_strategic_agent &lt;- function(state_size = 3, action_size = 3) {
  model &lt;- keras_model_sequential() %&gt;%
    layer_dense(units = 32, activation = &quot;relu&quot;, input_shape = c(state_size)) %&gt;%
    layer_dense(units = 32, activation = &quot;relu&quot;) %&gt;%
    layer_dense(units = action_size, activation = &quot;linear&quot;)
  
  model %&gt;% compile(
    optimizer = optimizer_adam(learning_rate = 0.001),
    loss = &quot;mse&quot;
  )
  return(model)
}

# Initialize the competing agents
agent_A &lt;- build_strategic_agent()
agent_B &lt;- build_strategic_agent()
</pre>


<h2 class="wp-block-heading">4. Parameterization & Historical Replay Buffer Ingestion</h2>



<p class="wp-block-paragraph">To anchor our agents in empirical reality, we pull 2 years of continuous daily settlement prices for Silver futures (<code>SI=F</code>). We define our microstructural bounds—such as the risk aversion parameter (gamma) and the permanent market impact vector (eta)—alongside a fixed strategic execution memory window (T = 10).</p>


<pre>
# 3. STRATEGIC PARAMETERS
T_horizon &lt;- 10      # Strategic episode length (Memory window)
gamma_param &lt;- 0.0001 # Risk aversion
eta_param &lt;- 0.0005   # Market impact

# 4. HISTORICAL REPLAY DATA (2-Year Training Set)
silver_full &lt;- tq_get(&quot;SI=F&quot;, from = Sys.Date() - 730) %&gt;%
  filter(!is.na(close)) %&gt;%
  mutate(returns = close / lag(close) - 1) %&gt;%
  drop_na()

# Recent window for the final audit visualization
silver_recent &lt;- tail(silver_full, T_horizon)
</pre>


<h2 class="wp-block-heading">5. Dynamic Volatility Corridors</h2>



<p class="wp-block-paragraph">Rather than mapping market behavior against static thresholds, the audit engine computes a volatility-adaptive safety corridor. The boundaries dynamically expand and contract based on the asset’s realized standard deviation (sigma), isolating pure structural noise from intentional strategic maneuvers.</p>


<pre>
# 5. DYNAMIC SIGMA CORRIDORS
current_sigma &lt;- sd(silver_recent$returns, na.rm = TRUE)
if(is.na(current_sigma)) current_sigma &lt;- 0.01 

analysis_data &lt;- silver_recent %&gt;%
  mutate(
    twap_slope = current_sigma * 1.5, 
    nash_slope = current_sigma * 4.0,
    twap_path = first(close) * (1 - seq(0, first(twap_slope), length.out = n())),
    nash_path = first(close) * (1 - seq(0, first(nash_slope), length.out = n())),
    lower_safety_limit = nash_path * (1 - current_sigma)
  )
</pre>


<h2 class="wp-block-heading">6. The Joint Training Replay Engine & Payoff Matrix</h2>



<p class="wp-block-paragraph">This section represents the computational implementation of Koulouris & Campajola’s memory hypothesis. The two agents recursively traverse 2 years of rolling historical windows (<code>window_data</code>).</p>



<p class="wp-block-paragraph">At each node, they sample independent actions based on their weights, facing a non-cooperative game matrix:</p>



<ul class="wp-block-list">
<li><strong>Mutual Cooperation (Action 0, 0):</strong> High joint payout (+10) mimicking a stable, supra-competitive margin.</li>



<li><strong>Mutual Aggressive Competition (Action Match):</strong> Low joint rent (+1), representing the competitive Nash baseline.</li>



<li><strong>Cheating / Under-cutting:</strong> Asymmetric penalization (+5 vs -5).</li>
</ul>


<pre>
# 6. JOINT TRAINING ENGINE (Symmetric Memory Interaction)
message(&quot;Joint Training: Agent A & Agent B are learning Silver Market dynamics...&quot;)

for(i in 1:(nrow(silver_full) - T_horizon)) {
  window_data &lt;- silver_full[i:(i + T_horizon - 1), ]
  vol &lt;- sd(window_data$returns, na.rm = TRUE)
  if(is.na(vol)) vol &lt;- 0.01
  
  state_vec &lt;- matrix(c(1.0, vol, 0.5), nrow = 1)
  
  act_A &lt;- which.max(predict(agent_A, state_vec, verbose = 0)) - 1
  act_B &lt;- which.max(predict(agent_B, state_vec, verbose = 0)) - 1
  
  rewards &lt;- if(act_A == 0 &#038;&#038; act_B == 0) {
    list(A = 10, B = 10) 
  } else if(act_A == act_B) {
    list(A = 1, B = 1)   
  } else {
    if(act_A &gt; act_B) list(A = 5, B = -5) else list(A = -5, B = 5) 
  }
  
  target_A &lt;- predict(agent_A, state_vec, verbose = 0)
  target_B &lt;- predict(agent_B, state_vec, verbose = 0)
  
  target_A[1, act_A + 1] &lt;- rewards$A
  target_B[1, act_B + 1] &lt;- rewards$B
  
  agent_A %&gt;% fit(state_vec, target_A, epochs = 1, verbose = 0)
  agent_B %&gt;% fit(state_vec, target_B, epochs = 1, verbose = 0)
}
</pre>


<h2 class="wp-block-heading">7. Post-Convergence Audit Inference & Regime Selection</h2>



<p class="wp-block-paragraph">Once the networks stabilize, the engine takes the posture of an unbiased financial regulator. It extracts the neural policy configurations, evaluates the actual current execution window, and automatically determines the market regime using an automated classification layer.</p>


<pre>
# 7. FINAL AUDIT INFERENCE
analysis_data &lt;- analysis_data %&gt;%
  rowwise() %&gt;%
  mutate(
    state_v = list(matrix(c(close/twap_path, current_sigma, (T_horizon - row_number())/T_horizon), nrow = 1)),
    q_A = list(predict(agent_A, state_v[], verbose = 0)),
    q_B = list(predict(agent_B, state_v[], verbose = 0)),
    joint_action = (which.max(q_A[]) + which.max(q_B[])) / 2
  ) %&gt;% ungroup()

# 8. STATUS LOGIC (Professional Category Selection & Color Alignment)
last_row &lt;- tail(analysis_data, 1)
market_status &lt;- case_when(
  last_row$close &gt;= last_row$twap_path ~ 
    list(
      label = &quot;**COOPERATIVE:** Pareto-Efficient Alignment&quot;, 
      bg    = &quot;#E8F8F5&quot;,  
      color = &quot;#27AE60&quot;   
    ),
  
  last_row$close &lt; last_row$twap_path &#038; last_row$close &gt;= last_row$nash_path ~ 
    list(
      label = &quot;**NORMAL:** Competitive Nash Equilibrium&quot;, 
      bg    = &quot;#FEF5E7&quot;,  
      color = &quot;#E67E22&quot;   
    ),
  
  TRUE ~ 
    list(
      label = &quot;**LIQUIDITY SHOCK:** Strategic Deviation Detected&quot;, 
      bg    = &quot;#FDEDEC&quot;,  
      color = &quot;#C0392B&quot;   
    )
)
</pre>


<h2 class="wp-block-heading">8. High-Fidelity Infographic Layer</h2>



<p class="wp-block-paragraph">To generate a publication-quality static vector infographic, we map our theme directly via <code>ggplot2</code> and <code>ggtext</code>. By embedding the color palette directly into the HTML subtitle strings and forcing label formatting via <code>scales::percent</code>, we create a clean, high-contrast dashboard visualization.</p>


<pre>
# 9. GGPLOT PRODUCTION VISUALIZATION (Static Mode with ggtext Integration)
ggplot(analysis_data, aes(x = date)) +
  geom_ribbon(aes(ymin = lower_safety_limit, ymax = twap_path), fill = &quot;darkgray&quot;, alpha = 0.3) +
  
  geom_line(aes(y = twap_path, color = &quot;TWAP (Cooperative)&quot;), size = 1) +
  geom_line(aes(y = nash_path, color = &quot;Nash (Competitive)&quot;), size = 1) +
  geom_line(aes(y = close, color = &quot;Actual Price&quot;), size = 1.3) +
  scale_y_continuous(labels = scales::label_currency()) +
  
  geom_richtext(
    aes(x = median(date), y = max(close, twap_path) * 1.02, label = market_status$label),
    fill = market_status$bg, color = market_status$color, size = 4,
    family = &quot;Roboto Slab&quot; 
  ) +
  
  scale_color_manual(
    name = NULL,
    values = c(&quot;Actual Price&quot; = &quot;steelblue&quot;, &quot;TWAP (Cooperative)&quot; = &quot;#27AE60&quot;, &quot;Nash (Competitive)&quot; = &quot;#E67E22&quot;)
  ) +
  
  labs(
    title = &quot;Silver Market Strategic Audit Engine&quot;,
    subtitle = paste0(
      &quot;&lt;span style=&#039;color:#27AE60;&#039;&gt;─── **Cooperative Zone**&lt;/span&gt; | &quot;,
      &quot;&lt;span style=&#039;color:#E67E22;&#039;&gt;─── **Competitive Zone**&lt;/span&gt; | &quot;,
      &quot;&lt;span style=&#039;color:steelblue;&#039;&gt;─── **Actual Execution**&lt;/span&gt;&lt;br&gt;&lt;br&gt;&quot;,
      &quot;&lt;span style=&#039;color:darkgrey;&#039;&gt;**Strategic Corridor** (Supra-Competitive Margin Zone)&lt;/span&gt;&quot;
    ),
    x = NULL, y = NULL,
    caption = glue(&quot;Dynamic Sigma: {scales::percent(current_sigma, accuracy = 0.01)} | Shortfall: {round(actual_cost, 2)}%&quot;)
  ) +
  
  theme_minimal(base_family = &quot;Roboto Slab&quot;) +
  theme(plot.title = element_text(face = &quot;bold&quot;, size = 16),
        plot.subtitle = element_markdown(face = &quot;bold&quot;), 
        axis.text = element_text(face = &quot;bold&quot;),
        legend.position = &quot;none&quot;)
</pre>


<figure data-wp-context="{"imageId":"6a1ed21ecb192"}" data-wp-interactive="core/image" data-wp-key="6a1ed21ecb192" class="wp-block-image size-large wp-lightbox-container"><img loading="lazy" data-attachment-id="12146" data-permalink="https://datageeek.com/2026/06/02/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/strategic_silver/" data-orig-file="https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png" data-orig-size="1110,646" data-comments-opened="1" data-image-meta="{"aperture":"0","credit":"","camera":"","caption":"","created_timestamp":"0","copyright":"","focal_length":"0","iso":"0","shutter_speed":"0","title":"","orientation":"0","alt":""}" data-image-title="strategic_silver" data-image-description="" data-image-caption="" data-large-file="https://i1.wp.com/datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=450&#038;ssl=1" data-wp-class--hide="state.isContentHidden" data-wp-class--show="state.isContentVisible" data-wp-init="callbacks.setButtonStyles" data-wp-on--click="actions.showLightbox" data-wp-on--load="callbacks.setButtonStyles" data-wp-on--pointerdown="actions.preloadImage" data-wp-on--pointerenter="actions.preloadImageWithDelay" data-wp-on--pointerleave="actions.cancelPreload" data-wp-on-window--resize="callbacks.setButtonStyles" src="https://i1.wp.com/datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=450&#038;ssl=1" alt="" class="wp-image-12146" srcset_temp="https://i1.wp.com/datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=450&#038;ssl=1 1024w, https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=150 150w, https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=300 300w, https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=768 768w, https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png 1110w" sizes="(max-width: 1024px) 100vw, 1024px" data-recalc-dims="1" /><button
			class="lightbox-trigger"
			type="button"
			aria-haspopup="dialog"
			data-wp-bind--aria-label="state.thisImage.triggerButtonAriaLabel"
			data-wp-init="callbacks.initTriggerButton"
			data-wp-on--click="actions.showLightbox"
			data-wp-style--right="state.thisImage.buttonRight"
			data-wp-style--top="state.thisImage.buttonTop"
		>
			<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" fill="none" viewBox="0 0 12 12">
				<path fill="#fff" d="M2 0a2 2 0 0 0-2 2v2h1.5V2a.5.5 0 0 1 .5-.5h2V0H2Zm2 10.5H2a.5.5 0 0 1-.5-.5V8H0v2a2 2 0 0 0 2 2h2v-1.5ZM8 12v-1.5h2a.5.5 0 0 0 .5-.5V8H12v2a2 2 0 0 1-2 2H8Zm2-12a2 2 0 0 1 2 2v2h-1.5V2a.5.5 0 0 0-.5-.5H8V0h2Z" />
			</svg>
		</button></figure>



<h2 class="wp-block-heading">9. Empirics & Compliance Conclusion</h2>



<p class="wp-block-paragraph">When we run the complete inference loop on our terminal Silver execution window, the strategic narrative clarifies perfectly: <strong>Actual Execution</strong> (the blue trajectory) tracks downward, bypassing the cooperative upper envelope and adhering directly to the competitive boundaries.</p>



<p class="wp-block-paragraph">The audit badge cleanly returns a status of <strong>NORMAL: Competitive Nash Equilibrium</strong>, with the terminal metrics computing the exact execution shortfall at <strong>1.59%</strong> as indicated in the chart above. While the agents are technically complex neural networks capable of learning memory patterns, the actual price action during this specific ten-day horizon reflects a highly competitive regime, keeping the execution within standard Nash boundaries rather than shifting into a supra-competitive zone.</p>



<p class="wp-block-paragraph">For quantitative auditors and systemic risk monitors, this approach signals a paradigm shift. Static threshold tests are blind to multi-agent learning trends. By deploying neural simulation baselines, structural compliance teams can automatically audit execution algorithms, isolating algorithmic alignment from pure market variance.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://datageeek.com/2026/06/02/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/"> DataGeeek</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/a-multi-agent-ddqn-strategic-audit-engine-for-silver-markets-using-keras-tensorflow/">A Multi-Agent DDQN Strategic Audit Engine for Silver Markets using Keras/Tensorflow</a>]]></content:encoded>
					
		
		<enclosure url="https://datageeek.com/wp-content/uploads/2026/06/datageeek-6a1ecc01a4357.png" length="0" type="" />
<enclosure url="https://1.gravatar.com/avatar/db5e3f9ef188ea98fe38ab05c5a3fad9fb52fe3472715a8fc02f7ea41731f77c?s=96&#038;d=identicon&#038;r=G" length="0" type="" />
<enclosure url="https://datageeek.com/wp-content/uploads/2026/06/strategic_silver.png?w=1024" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401602</post-id>	</item>
		<item>
		<title>11 Test Smells That Make Your Tests Lie to You</title>
		<link>https://www.r-bloggers.com/2026/06/11-test-smells-that-make-your-tests-lie-to-you/</link>
		
		<dc:creator><![CDATA[Jakub Sobolewski]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://jakubsobolewski.com/blog/test-smells-in-r</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Learn to recognize problems in R test code that cause your test suite to pass while hiding real bugs. Detect those issues and start writing more trustworthy tests.</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/11-test-smells-that-make-your-tests-lie-to-you/">11 Test Smells That Make Your Tests Lie to You</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://jakubsobolewski.com/blog/test-smells-in-r"> Jakub Sobolewski</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p><img src="https://i0.wp.com/jakubsobolewski.com/blog/test-smells-in-r/og-image.png?w=578&#038;ssl=1" alt="11 Test Smells That Make Your Tests Lie to You" data-recalc-dims="1" /></p><p>Green tests don’t automatically mean correct code.</p>
<p>Tests can be written in ways that pass reliably, survive every <code>devtools::test()</code> run, and still tell you almost nothing about whether your code works. The software engineering community has a name for these patterns: test smells. They’re structural problems in test code — not bugs, but patterns that signal the tests are doing less or something different than they appear.</p>
<p>Have you ever reviewed a pull request and only skimmed the test code, because why bother if you see in CI they’re green?</p>
<p>The problem with test code is that:</p>
<ul>
<li>it can be written in ways that it looks fine but is actually misleading,</li>
<li>you may not review test code with the same seriousness as production code,</li>
<li>you may not know what to look for in test code.</li>
</ul>
<p>Here’s a list of problems in test code that show up regularly, and what to do about them.</p>
<h2 id="1-mystery-guest">1. Mystery Guest</h2>
<p><strong>🫣 The smell:</strong> the test relies on state that isn’t visible inside the test itself.</p>
<pre>test_that(&quot;report uses the correct currency symbol&quot;, {
  result &lt;- format_currency(1000)
  expect_equal(result, &quot;£1,000&quot;)
})</pre>
<p>This test passes on one machine and fails on another — and the reason isn’t in the test. Somewhere upstream, a <code>.Rprofile</code> file sets <code>options(currency = &quot;GBP&quot;)</code>, or maybe it’s set with an environment variable. In a fresh session without that state, <code>format_currency</code> falls back to <code>&quot;$1,000&quot;</code>.</p>
<p>The test has an invisible guest. You can’t understand it by reading it.</p>
<p>The fix is to make the dependency explicit — set it and clean it up within the test itself using <code>withr</code>:</p>
<pre>test_that(&quot;report uses the correct currency symbol&quot;, {
  withr::with_options(list(currency = &quot;GBP&quot;), {
    result &lt;- format_currency(1000)
    expect_equal(result, &quot;£1,000&quot;)
  })
})</pre>
<p>It might be also a default value in the function itself that is critical to the behavior being tested. In that case, the test should set it explicitly:</p>
<pre>test_that(&quot;report uses the correct currency symbol&quot;, {
  # Arrange
  currency &lt;- &quot;GBP&quot;
  amount &lt;- 1000

  # Act
  result &lt;- format_currency(amount, currency = currency)

  # Assert
  expect_equal(result, &quot;£1,000&quot;)
})</pre>
<p>Now the test tells its own story. Anyone reading it knows exactly what conditions it relies on.</p>
<h2 id="2-eager-test">2. Eager Test</h2>
<p><strong>🫣 The smell:</strong> one test that asserts on multiple unrelated behaviors.</p>
<pre>test_that(&quot;data pipeline works&quot;, {
  result &lt;- run_pipeline(raw_data)
  expect_equal(nrow(result), 100)
  expect_equal(ncol(result), 5)
  expect_true(all(!is.na(result$value)))
  expect_equal(result$category, expected_categories)
  expect_true(is.numeric(result$value))
})</pre>
<p>This test is checking the row count, the column count, the NA handling, the category values, and the column type — all under the title “data pipeline works.” When it fails, the output tells you <em>something</em> broke. Not <em>what</em>.</p>
<p>Split it into cases where each title describes the specific behavior:</p>
<pre>test_that(&quot;pipeline returns 100 rows&quot;, {
  # Arrange
  raw_data &lt;- ...

  # Act
  result &lt;- run_pipeline(raw_data)

  # Assert
  expect_equal(nrow(result), 100)
})

test_that(&quot;pipeline removes rows with missing values&quot;, {
  # Arrange
  raw_data &lt;- ...

  # Act
  result &lt;- run_pipeline(raw_data)

  # Assert
  expect_true(all(!is.na(result$value)))
})

test_that(&quot;pipeline produces numeric values&quot;, {
  # Arrange
  raw_data &lt;- ...

  # Act
  result &lt;- run_pipeline(raw_data)

  # Assert
  expect_true(is.numeric(result$value))
})</pre>
<p>This test suite has more lines, yes. Is it bad? No.</p>
<p>We’re not optimising memory, we’re optimising for clarity. It’s also much easier to maintain — when a requirement changes, you know exactly which test to update and why.</p>
<p>Adding <code># Arrange</code>, <code># Act</code>, and <code># Assert</code> comments is a simple habit that makes this smell hard to miss. When the <code># Assert</code> block has five <code>expect_*</code> calls testing unrelated things, the label might prompt you to notice — and question whether they all belong together. When the <code># Act</code> creates multiple results, it becomes clear that the test is doing too much.</p>
<p>The structure doesn’t fix the test, but it makes the problem easier to spot at a glance. The test is no longer a blob of code doing everything at once. It has a structure that invites you to think about what belongs in each section.</p>
<h2 id="3-over-specification">3. Over-specification</h2>
<p><strong>🫣 The smell:</strong> the test checks <em>how</em> the code does something, not <em>what</em> it produces.</p>
<pre>test_that(&quot;sends notification on upload&quot;, {
  # Arrange
  mock_notify &lt;- mockery::mock()
  mockery::stub(process_upload, &quot;notify_user&quot;, mock_notify)

  # Act
  process_upload(file = &quot;report.csv&quot;, user_id = &quot;u42&quot;)

  # Assert
  mockery::expect_called(mock_notify, n = 1)
  mockery::expect_call(
    mock_notify,
    1,
    notify_user(&quot;u42&quot;, type = &quot;upload_complete&quot;)
  )
})</pre>
<p>This test will break the moment you rename <code>notify_user</code>, change its signature, or switch to a different notification mechanism internally — even if the <em>behavior</em> (the user gets notified) stays exactly the same.</p>
<p>Over-specified tests are tightly coupled to implementation. They punish refactoring.</p>
<p>The alternative is to test from the outside:</p>
<pre>test_that(&quot;user receives notification after upload&quot;, {
  # Arrange
  notifications &lt;- list()

  # Act
  process_upload(
    file = &quot;report.csv&quot;,
    user_id = &quot;u42&quot;,
    notify_user = function(user_id, ...) {
      notifications[[length(notifications) + 1]] &lt;&lt;- user_id
    }
  )

  # Assert
  expect_length(notifications, 1)
  expect_equal(notifications[[1]], &quot;u42&quot;)
})</pre>
<p>The test no longer cares how notification happens internally — just that <code>&quot;u42&quot;</code> received one. You can refactor the internals freely.</p>
<p>Notice what changed structurally: instead of patching <code>notify_user</code> after the fact with a stub, <code>process_upload</code> now accepts it as an argument. This is dependency injection, and it’s worth understanding as a pattern in its own right — not just as a way to avoid over-specification, but as a replacement for stubs and mocks altogether.</p>
<p>When a dependency is injected, the test passes in exactly the implementation it wants. There’s no patching, no intercepting, no targeting of internal function names. The test controls the dependency directly:</p>
<pre>process_upload &lt;- function(file, user_id, notify_user = real_notify_user) {
  # ... upload logic
  notify_user(user_id, type = &quot;upload_complete&quot;)
}</pre>
<p>In production, <code>real_notify_user</code> is used. In tests, any function with the same interface can be passed in. The key word is <em>interface</em>: the argument name and what it receives. That’s the contract between <code>process_upload</code> and whatever handles notifications.</p>
<p>This changes how test failures work — in a good way. With a stub, the test fails when the internal call site changes: rename the function, move it to a different module, inline it, and the stub can no longer target it. The failure is about test plumbing, not about behavior. With dependency injection, the test only fails when the <em>interface</em> changes — when <code>notify_user</code> is expected to receive different arguments, or when it’s removed entirely. That’s a failure you want to know about. It means the contract between components changed, and the fake dependency needs updating to match.</p>
<p>Notice also that the fake <code>notify_user</code> is just a plain function — no mocking library needed. A mock that accepts any call and records nothing will let over-specification creep back in through <code>expect_called()</code> and <code>expect_call()</code>. A simple fake that does one concrete thing — appends to a list, increments a counter, writes to a local variable — can only be asserted on from the outside. It doesn’t offer the machinery to inspect internals, so the test can’t become over-specified even if you wanted it to.</p>
<h2 id="4-sensitive-equality">4. Sensitive Equality</h2>
<p><strong>🫣 The smell:</strong> the test asserts on an entire object when only part of it matters.</p>
<pre>test_that(&quot;filters inactive users&quot;, {
  # Arrange
  users &lt;- ...

  # Act
  result &lt;- filter_active_users(users)

  # Assert
  expect_equal(result, data.frame(...))
})</pre>
<p>The expectation was created when this test was first written. Since then, the <code>users</code> data frame has gained two new columns. Every test asserting against the full snapshot now fails — not because the filtering logic is wrong, but because the expected object is stale.</p>
<p>Asserting on a whole object isn’t always wrong.</p>
<p>A test that documents the full shape of the output — its columns, types, and structure — is genuinely useful. It acts as a contract: this is what <code>filter_active_users</code> returns. When that contract changes intentionally, the test tells you.</p>
<p>This matters especially in R. In a statically typed language the compiler enforces what a function returns — change the return type and the code won’t build. R has no such guarantee. A function can return a data frame today and a list tomorrow, drop a column, silently change a type, and nothing outside the tests will catch it.</p>
<p>A full-object contract test fills the loose-typing gap, giving you the kind of output verification the language doesn’t provide for free.</p>
<p>The smell appears when <em>every</em> test does this, or when the full-object assertion is used to check a behavior where most of the fields are irrelevant. If you’re testing that the filter removed inactive users, the output’s column names and data types are noise — and noise breaks tests for the wrong reasons.</p>
<p>A cleaner split is one test for the shape, separate tests for the behaviors:</p>
<pre>test_that(&quot;returns a data frame with user_id and active columns&quot;, {
  # Arrange
  users &lt;- ...

  # Act
  result &lt;- filter_active_users(users)

  # Assert
  expect_equal(result, expected_users)
})

test_that(&quot;filters inactive users&quot;, {
  # Arrange
  users &lt;- ...

  # Act
  result &lt;- filter_active_users(users)

  # Assert
  expect_equal(result$user_id, c(&quot;u01&quot;, &quot;u03&quot;, &quot;u07&quot;))
  expect_true(all(result$active))
})</pre>
<p>The first test owns the full-object assertion and will break when the output shape changes — which is the right test to break. The second test owns the filtering logic and only breaks when the filtering logic is wrong – not when an irrelevant column is added or renamed.</p>
<h2 id="5-obscure-test">5. Obscure Test</h2>
<p><strong>🫣 The smell:</strong> the setup is so long that the behavior being tested is buried inside it.</p>
<pre>test_that(&quot;calculates discount correctly&quot;, {
  user &lt;- list(
    id = &quot;u42&quot;,
    name = &quot;Alice&quot;,
    email = &quot;alice@example.com&quot;,
    created_at = as.Date(&quot;2020-01-01&quot;),
    plan = &quot;pro&quot;,
    region = &quot;EU&quot;,
    active = TRUE,
    referral_code = &quot;REF001&quot;,
    payment_method = &quot;card&quot;,
    currency = &quot;EUR&quot;
  )
  product &lt;- list(
    id = &quot;p99&quot;,
    name = &quot;Annual Subscription&quot;,
    base_price = 200,
    category = &quot;software&quot;,
    tax_rate = 0.2,
    available = TRUE
  )
  result &lt;- calculate_discount(user, product)
  expect_equal(result$discount_pct, 0.15)
})</pre>
<p>By the time you reach the assertion, you’ve waded through 20 lines of setup. Why does the user need a <code>referral_code</code>? Does <code>currency</code> affect the discount? Is <code>tax_rate</code> relevant? The intent is invisible.</p>
<p>The fix is to extract a builder that encodes sensible defaults and lets each test express only what’s relevant to that case:</p>
<pre># If only used in this test file, keep alongside the tests.
# If shared across multiple files, move to a `setup-*.R` file.
make_user &lt;- function(...) {
  defaults &lt;- list(
    id = &quot;u_default&quot;, name = &quot;Test User&quot;,
    plan = &quot;basic&quot;, region = &quot;EU&quot;, active = TRUE,
    currency = &quot;EUR&quot;, payment_method = &quot;card&quot;
  )
  modifyList(defaults, list(...))
}

test_that(&quot;pro users receive a 15% discount&quot;, {
  # Arrange
  user &lt;- make_user(plan = &quot;pro&quot;)
  product &lt;- list(
    id = &quot;p99&quot;,
    base_price = 200,
    category = &quot;software&quot;
  )

  # Act
  result &lt;- calculate_discount(user, product)

  # Assert
  expect_equal(result$discount_pct, 0.15)
})</pre>
<p>Now the test title and the setup agree: this is a test about the <code>&quot;pro&quot;</code> plan. Everything else is noise that’s been moved out of sight.</p>
<p>Using <code># Arrange</code>, <code># Act</code>, and <code># Assert</code> comments here also helps. The test is no longer a blob of code doing everything at once. It has a structure that invites you to think about what belongs in each section, and helps you understand when one secttion ends and another begins. The test is easier to read, and the intent is clearer.</p>
<h2 id="6-meaningless-title">6. Meaningless Title</h2>
<p><strong>🫣 The smell:</strong> the test name describes what the code does, not what behavior it verifies.</p>
<pre>test_that(&quot;calculate_discount works&quot;, {
  # Arrange
  user &lt;- make_user(plan = &quot;pro&quot;)

  # Act
  result &lt;- calculate_discount(user, product)

  # Assert
  expect_equal(result$discount_pct, 0.15)
})</pre>
<p>This is by far the most common test smell I see.</p>
<p>Naming things is hard. I don’t blame anyone for struggling to find a good test title. But a meaningless title is as useless as no title at all. Let’s make some effort here! If we can pause to come up with a good name for a function — we can think of a name for a test too.</p>
<p><code>&quot;calculate_discount works&quot;</code> is not a test title — it’s a function name with “works” appended. When this test fails, the output tells you that <code>calculate_discount</code> stopped working. It doesn’t tell you <em>what it’s supposed to do</em> or <em>under what conditions it broke</em>.</p>
<p>The title is the first thing you read when a test fails. It should answer two questions: <em>what behavior is expected</em>, and <em>under what circumstances</em>.</p>
<pre>test_that(&quot;pro users receive a 15% discount&quot;, {
  # Arrange
  user &lt;- make_user(plan = &quot;pro&quot;)

  # Act
  result &lt;- calculate_discount(user, product)

  # Assert
  expect_equal(result$discount_pct, 0.15)
})</pre>
<p>Now the failure output reads: <code>pro users receive a 15% discount</code> — and you immediately know whether the logic is wrong or the test is outdated.</p>
<p>This smell is easy to spot: if your test title could be swapped with any other test on the same function without anyone noticing, it’s not a title — it’s a placeholder. A good title is a specification. It should still make sense even if the implementation is completely rewritten.</p>
<p>Some patterns that signal the smell:</p>
<ul>
<li>Ends with “works”, “is correct”, “should work”, or “test”</li>
<li>Is just the function name</li>
<li>Doesn’t mention the input condition that makes this case distinct</li>
</ul>
<hr>
<p><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>A rule of thumb: write the title before you write the test body. If you can’t name what you’re testing, you don’t know what you’re testing yet.</strong></p>
<hr>
<h2 id="7-flaky-test">7. Flaky Test</h2>
<p><strong>🫣 The smell:</strong> the test sometimes passes and sometimes fails without any code change.</p>
<pre>test_that(&quot;model selects the most important features&quot;, {
  # Arrange
  training_data &lt;- ...
  top_n &lt;- 3

  # Act
  result &lt;- select_features(training_data, top_n = top_n)

  # Assert
  expect_equal(result$feature, c(&quot;age&quot;, &quot;income&quot;, &quot;region&quot;))
})</pre>
<p>This passes on the developer’s machine today. Tomorrow it returns <code>c(&quot;income&quot;, &quot;age&quot;, &quot;region&quot;)</code> — same features, different order — because two of them had near-identical importance scores and a random tie-breaking step inside the model produced a different ranking. The test fails. Nothing is broken.</p>
<p>Flaky tests are one of the most corrosive smells in a test suite. They don’t just fail — they train developers to distrust failures. Once the team learns to re-run tests until they go green, a genuinely broken build can hide for days.</p>
<p>The R-specific sources of flakiness to watch for: unset random seeds, tests with hard-coded <code>Sys.sleep()</code> waits that assume the app loads within a fixed time, tests that rely on network availability, and assertions against system time or floating-point values without a tolerance.</p>
<p>For the feature selection example, the fix is to set the random seed before the call, and to assert on what actually matters — that the right features were selected — rather than the incidental order they came back in:</p>
<pre>test_that(&quot;model selects age, income, and region as top features&quot;, {
  # Arrange
  training_data &lt;- ...
  top_n &lt;- 3
  withr::local_seed(42)

  # Act
  result &lt;- select_features(training_data, top_n = top_n)

  # Assert
  expect_equal(result$feature, c(&quot;age&quot;, &quot;income&quot;, &quot;region&quot;))
})</pre>
<p>Assuming it’s randomness that makes the test flaky, setting the seed makes any remaining randomness deterministic. The test now passes or fails for the right reason.</p>
<h2 id="8-never-failing-test">8. Never-Failing Test</h2>
<p><strong>🫣 The smell:</strong> the test will pass no matter what the code does.</p>
<pre>test_that(&quot;returns an error for invalid input&quot;, {
  expect_error(calculate_discount(user = NULL, product))
})</pre>
<p>This passes today. It will also pass if <code>calculate_discount</code> is rewritten to throw a completely different error, emit a warning instead, return <code>NA</code> silently, or crash with an unrelated message. The assertion only checks that <em>some</em> error occurred — not the right one.</p>
<p>The same trap appears with positive assertions:</p>
<pre>test_that(&quot;result is a list&quot;, {
  result &lt;- run_pipeline(data)
  expect_true(is.list(result))
})</pre>
<p>If <code>run_pipeline</code> returns <em>any</em> list — empty, malformed, missing all expected fields — this test will almost always pass. It is not testing behavior; it is testing that R’s type system is still working.</p>
<p>Always assert on the specific outcome. For errors, check the message:</p>
<pre>test_that(&quot;returns an error when user is NULL&quot;, {
  # Arrange
  user &lt;- NULL
  product &lt;- ...

  # Act, Assert
  expect_error(
    calculate_discount(user = NULL, product),
    &quot;user must not be NULL&quot;
  )
})</pre>
<p>For positive assertions, check the values that actually matter — not just the type that contains them.</p>
<h2 id="9-test-order-dependency">9. Test Order Dependency</h2>
<p><strong>🫣 The smell:</strong> tests only pass when run in a specific sequence.</p>
<pre>test_that(&quot;initialises the cache&quot;, {
  initialise_cache()
  expect_true(cache_is_ready())
})

test_that(&quot;stores a value in the cache&quot;, {
  store_in_cache(&quot;key&quot;, &quot;value&quot;)
  expect_equal(retrieve_from_cache(&quot;key&quot;), &quot;value&quot;)
})</pre>
<p>The second test silently depends on the first having run. If you run it in isolation it fails with an error about the cache not being initialized, which looks like a cache bug but is actually a test structure bug.</p>
<p>The symptom is tests that pass with <code>devtools::test()</code> and fail with <code>testthat::test_file()</code> on a single file, or tests that break when you move them to a different <code>describe()</code> block.</p>
<p>The fix is to make each test fully self-contained. Every test should set up whatever it needs and leave no state behind:</p>
<pre>test_that(&quot;stores a value in the cache&quot;, {
  # Arrange
  initialise_cache()
  withr::defer(clear_cache())
  store_in_cache(&quot;key&quot;, &quot;value&quot;)

  # Act
  value &lt;- retrieve_from_cache(&quot;key&quot;)

  # Assert
  expect_equal(value, &quot;value&quot;)
})</pre>
<p><code>withr::defer()</code> ensures the cleanup runs even if the test fails, so the next test always starts from a clean slate.</p>
<h2 id="10-conditional-test-logic">10. Conditional Test Logic</h2>
<p><strong>🫣 The smell:</strong> the test body contains <code>if</code>, <code>else</code>, or <code>tryCatch</code>.</p>
<pre>test_that(&quot;formats output correctly&quot;, {
  result &lt;- format_output(data)
  if (nrow(result) &gt; 0) {
    expect_equal(result$label[[1]], &quot;Total&quot;)
  } else {
    expect_equal(nrow(result), 0)
  }
})</pre>
<p>When a test branches, it is multiple tests in one — and only one branch executes on any given run. The other path is permanently untested. If the data changes so that <code>nrow(result)</code> is always <code>0</code>, the label check never runs again and a regression in label formatting will go undetected forever.</p>
<p>Conditional logic in tests is also a sign the test doesn’t know what it expects. A test that hedges with <code>if</code> hasn’t decided what the correct behavior is.</p>
<p>If you find yourself using a <code>tryCatch</code>, then it seems like you don’t know whether the code should throw an error or not. Tests must know if an error is expected.</p>
<p>Split the conditions into separate, unconditional tests with setups that guarantee the state each one needs:</p>
<pre>test_that(&quot;includes a Total label when data is non-empty&quot;, {
  # Arrange
  non_empty_data &lt;- ...

  # Act
  result &lt;- format_output(non_empty_data)

  # Assert
  expect_equal(result$label[[1]], &quot;Total&quot;)
})

test_that(&quot;returns empty output when data is empty&quot;, {
  # Arrange
  empty_data &lt;- ...

  # Act
  result &lt;- format_output(empty_data)

  # Assert
  expect_equal(nrow(result), 0)
})</pre>
<p>Each test now executes its assertion unconditionally, every time.</p>
<h2 id="11-test-logic-in-production">11. Test Logic in Production</h2>
<p><strong>🫣 The smell:</strong> production code contains special branches that only exist to make tests pass.</p>
<pre>process_payment &lt;- function(
  amount,
  user_id,
  env = Sys.getenv(&quot;APP_ENV&quot;)
) {
  if (env == &quot;test&quot;) {
    return(list(
      status = &quot;ok&quot;,
      transaction_id = &quot;test-txn-001&quot;
    ))
  }
  # ... real payment logic
}</pre>
<p>The test environment gets a hardcoded success response. Production gets the real code. The two paths diverge immediately, so the tests are no longer testing what ships.</p>
<p>This smell often starts as a quick fix — “I’ll just skip the real API call in tests” — and ends up as a permanent fixture that no one dares remove. The production branch accumulates changes – the test branch stays frozen. At some point the tests are validating behavior that the production code no longer exhibits.</p>
<p>The correct fix is dependency injection: pass the thing you want to replace as an argument, and substitute it in tests:</p>
<pre>process_payment &lt;- function(
  amount,
  user_id,
  payment_gateway = real_gateway
) {
  result &lt;- payment_gateway(amount, user_id)
  list(status = result$status, transaction_id = result$id)
}

test_that(&quot;returns ok status on successful payment&quot;, {
  # Arrange
  fake_gateway &lt;- function(amount, user_id) {
    list(status = &quot;ok&quot;, id = &quot;txn-001&quot;)
  }
  amount &lt;- 100
  user_id &lt;- &quot;u42&quot;

  # Act
  result &lt;- process_payment(
    amount,
    user_id,
    payment_gateway = fake_gateway
  )

  # Assert
  expect_equal(result$status, &quot;ok&quot;)
})</pre>
<p>Production code has no awareness of tests. Tests control the dependency. Both paths exercise the same logic.</p>
<h2 id="the-pattern">The Pattern</h2>
<p>These smells fall into two categories. Some make your tests untrustworthy: they pass for reasons unrelated to whether the code works. Others make your tests unreadable: they pass and fail correctly but no one can tell what they mean.</p>
<p>A test is documentation. It tells the next developer — probably future you — what the code is supposed to do and under what conditions. A smelly test fails at that job before it ever fails at catching bugs.</p>
<p>The most useful question to ask while writing a test isn’t “will this pass?” but “if this fails at 2am, will I understand what broke and why?”</p>
<p>If the answer is no, the test has a smell worth fixing.</p>
<p>Here’s a quick reference to keep nearby:</p>





















































<table><thead><tr><th>Smell</th><th>Signal</th></tr></thead><tbody><tr><td>Mystery Guest</td><td>Test relies on state set somewhere else</td></tr><tr><td>Eager Test</td><td>One block, many unrelated assertions</td></tr><tr><td>Over-specification</td><td>Breaks on refactor, not on broken behavior</td></tr><tr><td>Sensitive Equality</td><td>Full-object assertion on a partial concern</td></tr><tr><td>Obscure Test</td><td>Setup buries the point of the test</td></tr><tr><td>Meaningless Title</td><td>Name doesn’t say what behavior is expected</td></tr><tr><td>Flaky Test</td><td>Passes or fails depending on timing or environment</td></tr><tr><td>Never-Failing Test</td><td>Will pass regardless of what the code does</td></tr><tr><td>Test Order Dependency</td><td>Only passes when run after another specific test</td></tr><tr><td>Conditional Test Logic</td><td><code>if</code>/<code>else</code> inside the test body</td></tr><tr><td>Test Logic in Production</td><td>Production code has special branches for tests</td></tr></tbody></table>
<p>None of these require a major rewrite to fix. Each one has a mechanical correction: extract the state, split the block, inject the dependency, narrow the assertion, introduce a builder, rename the case. The value is in learning to <em>see</em> them first.</p>
<p>Once you can spot these smells, you can start to eliminate them, one by one — and build a better test suite that tells you the truth about your code.</p>
<h2 id="apply-this">Apply this</h2>
<p>Reading about smells is easy. Spotting them in your own suite is the hard part. The fastest way to make them stick is to point an AI agent at your real test code, have it find the smells, then fix the worst few yourself — that’s where the learning happens.</p>
<p>Open a test file in your AI coding agent (Claude Code, Cursor, Copilot Chat) and paste this prompt:</p>
<pre>You are a senior R engineer reviewing test code for &quot;test smells&quot; — structural patterns that let tests pass while hiding bugs or obscuring intent.

Scope: audit the test file(s) I've shared. Also read the production code they exercise — several smells can't be judged from the test alone. Use testthat and withr idioms in your fixes.

Detect these 11 smells. For each, here's the tell and the fix I prefer:

1. Mystery Guest — depends on options / env vars / .Rprofile set elsewhere → set and scope it inside the test with withr::with_options / local_envvar, or pass the value in as an argument.
2. Eager Test — one test_that asserts many unrelated behaviors → split to one behavior per test_that, each titled with the behavior it checks.
3. Over-specification — asserts on internal calls via mockery::stub / mock (expect_called, expect_call) → inject a fake as an argument and assert on the observable result, not the call.
4. Sensitive Equality — full-object expect_equal where only part matters → assert the specific fields. Keep ONE deliberate full-object &quot;shape contract&quot; test; that one is legitimate, not a smell.
5. Obscure Test — long setup buries the point → extract a make_*() builder with modifyList defaults, set only the field under test; move shared builders to setup-*.R.
6. Meaningless Title — title is &quot;&lt;fn&gt; works&quot; or just the function name → rewrite to state the expected behavior and the condition; it should still make sense if the implementation were rewritten.
7. Flaky Test — depends on seed / time / network / order, or asserts exact floats or incidental ordering → set withr::local_seed(), add a tolerance, assert on what matters not the order it came back in.
8. Never-Failing Test — expect_error() with no expected message, or asserts only a type (is.list, is.numeric) → assert the specific error message and the actual values.
9. Test Order Dependency — relies on state another test left behind → make it self-contained; set up what it needs and clean up with withr::defer().
10. Conditional Test Logic — if / else / tryCatch in the test body → split into unconditional tests whose setup guarantees each branch's state.
11. Test Logic in Production — `if (env == &quot;test&quot;)`-style branches in the source → remove them and use dependency injection so tests and production run one path.

Rules:
- Only flag clear instances. Don't invent smells to look thorough, and don't flag the legitimate patterns above (injected fakes, a single shape-contract test, AAA comments).
- Quote the offending lines and cite file:line for every finding.
- Never change what the production code does — only its testability. If a fix needs a signature change (dependency injection), say so and describe the new interface.

Output:
1. A triage table: smell | file:line | severity (high/med/low) | one-line why.
2. Then fix the highest-severity findings as before/after code blocks — the smallest change that removes the smell. Stop after three and ask before continuing if more remain.
3. Tell me how to re-run just these tests to confirm they still pass.</pre>
<p>Before you commit a test, run it past this checklist:</p>
<ul class="contains-task-list">
<li class="task-list-item"><input type="checkbox" disabled> The title states the behavior and the condition, and would still make sense if the implementation were rewritten.</li>
<li class="task-list-item"><input type="checkbox" disabled> Everything the test depends on is visible inside the test (or its <code>withr</code> / <code>setup-*.R</code> scaffolding).</li>
<li class="task-list-item"><input type="checkbox" disabled> It asserts on a specific outcome — not just a type, and not just “some error happened.”</li>
<li class="task-list-item"><input type="checkbox" disabled> It passes or fails for exactly one reason, and that reason lives in the test, not in the run order.</li>
</ul>
<p>Want a checklist for the whole suite, not just one file? The <a href="https://jakubsobolewski.com/get-roadmap" rel="nofollow" target="_blank">R testing roadmap</a> turns these habits into a step-by-step path you can work through.</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://jakubsobolewski.com/blog/test-smells-in-r"> Jakub Sobolewski</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/11-test-smells-that-make-your-tests-lie-to-you/">11 Test Smells That Make Your Tests Lie to You</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401604</post-id>	</item>
		<item>
		<title>Refactoring with Jarl: a coffee chat</title>
		<link>https://www.r-bloggers.com/2026/06/refactoring-with-jarl-a-coffee-chat/</link>
		
		<dc:creator><![CDATA[Posts on R-hub blog]]></dc:creator>
		<pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://blog.r-hub.io/2026/06/02/jarl/</guid>

					<description><![CDATA[<p>TL;DR, don’t let your friends use LLMs for finding useless code in a project! Using Jarl instead is cheaper, more reliable, and won’t kill any kitten.<br />
We (Hannah and Maëlle) share an appreciation for the unglamorous maintenance work we c...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/refactoring-with-jarl-a-coffee-chat/">Refactoring with Jarl: a coffee chat</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://blog.r-hub.io/2026/06/02/jarl/"> Posts on R-hub blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<figure><img src="https://i2.wp.com/blog.r-hub.io/2026/06/02/jarl/meme.png?w=578&#038;ssl=1"
    alt="Cute kitten attacked by robots. The text says &#39;Everytime you use Claude for something a CLI can do, a kitten dies&#39;." data-recalc-dims="1">
</figure>

<p>TL;DR, don’t let your friends use LLMs for finding useless code in a project! Using <a href="https://jarl.etiennebacher.com/" rel="nofollow" target="_blank">Jarl</a> instead is cheaper, more reliable, and won’t kill any kitten.</p>
<p>We (Hannah and Maëlle) share an appreciation for the unglamorous maintenance work we call upkeep. So when Claude highlighted some <a href="https://github.com/tidymodels/tune/commit/5b4d63b58bd496e6c2809a5444b6ab119ae14f37" rel="nofollow" target="_blank">dead</a> <a href="https://github.com/tidymodels/tune/commit/16766b32ae1157425b124a70ec0bd64246e24c7f" rel="nofollow" target="_blank">code</a> in the tune package to Hannah, it was worth a mention during a chat. This led to Maëlle recommending Etienne Bacher’s <a href="https://jarl.etiennebacher.com/" rel="nofollow" target="_blank">Jarl</a>, a fast linter for R written in Rust that can, among other things, detect unused functions.</p>
<p>Motivated by this conversation among other things, Maëlle wrote a <a href="https://ropensci.org/blog/2026/04/02/tree-sitter-overview/" rel="nofollow" target="_blank">whole blog post</a> about Jarl and other tooling for R based on tree-sitter, including the above meme.</p>
<p>So naturally, when you get called out in a custom meme, you listen. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f638.png" alt="😸" class="wp-smiley" style="height: 1em; max-height: 1em;" /> So here’s the long(er) form of “Use Jarl!”, from the following sweep through the <a href="https://parsnip.tidymodels.org/" rel="nofollow" target="_blank">parsnip</a> package by Hannah, with commentary from Maëlle.</p>
<p><em>Many thanks to <a href="https://www.etiennebacher.com/" rel="nofollow" target="_blank">Etienne Bacher</a> for reviewing this post!</em></p>
<h2 id="how-hannah-used-jarl">How Hannah used Jarl</h2>
<p>I have the <a href="https://jarl.etiennebacher.com/howto/editors" rel="nofollow" target="_blank">Jarl extension</a> installed for my Positron but here I used the command line interface (CLI) of Jarl. I started off with running this <a href="https://jarl.etiennebacher.com/by-example#lint-a-directory" rel="nofollow" target="_blank">command</a> in the terminal:</p>
<pre>jarl check .
</pre><p>but that gave me too much output. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f605.png" alt="😅" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<p>It did include a helpful hint though, something like:</p>
<pre>── Summary ──────────────────────────────────────
Found 296 errors.
3 fixable with the `--fix` option.
More than 15 errors reported, use `--statistics` to get the count by rule.
</pre><p>So one more time, with the <code>--statistics</code> option, to get an overview of what Jarl all flagged. The output (here with made up numbers) looked something like</p>
<pre>jarl check . --statistics
  132 [ ] implicit_assignment
  109 [ ] internal_function
   34 [ ] vector_logic
   13 [ ] unused_function
    7 [*] numeric_leading_zero
    1 [*] any_is_na

Rules with `[*]` have an automatic fix.
</pre><p>Now that gives me a better overall picture. <em>Maëlle: I really enjoyed learning about this <code>--statistics</code> flag, and followed the same strategy when I ran Jarl on the <a href="https://github.com/igraph/rigraph/issues/2652" rel="nofollow" target="_blank">igraph R package</a>!</em></p>
<p>I could have run <code>jarl check . --fix</code> to fix everything with an automatic fix but I wanted to learn a bit more about the rules to see if I wanted to apply them. So I worked my way through one rule at a time, with one git commit at a time. You can <a href="https://jarl.etiennebacher.com/by-example#select-or-ignore-specific-rules" rel="nofollow" target="_blank">select rules</a> like so:</p>
<pre>jarl check . --select any_is_na
</pre><p>This separation of changes by rule also made it easier to review changes: one topic per commit. <em>Maëlle: in igraph, I created one <strong>PR</strong> per rule or group of rules.</em></p>
<p>I first went through the ones with automatic fixes, then the ones without. The result of the clean-up: <a href="https://github.com/tidymodels/parsnip/pull/1356" rel="nofollow" target="_blank">https://github.com/tidymodels/parsnip/pull/1356</a> <em>Maëlle: in igraph, I went through rules starting with the ones with the least violations, ending with the ones that had the most hits.</em></p>
<p>parsnip has been around the block a few times and had accumulated a few functions flagged by Jarl as unused. I spotted <code>release_bullets()</code> in the list which I know we want to keep.<sup id="fnref:1"><a href="https://blog.r-hub.io/2026/06/02/jarl/#fn:1" class="footnote-ref" role="doc-noteref" rel="nofollow" target="_blank">1</a></sup> We use it <em>for</em> parsnip in the release process but we don’t use it <em>in</em> parsnip. I wasn’t expecting Jarl to catch onto this difference so I decided to review the list of flagged functions separately and split that off into its own issue (and PR). I ended up keeping one other function that was part of a set of functions provided in a standalone file but I removed the other ones.</p>
<p>No kittens were hurt!</p>
<h2 id="why-use-jarl-for-refactoring">Why use Jarl for refactoring</h2>
<p>Obviously, because we are telling you you should! More seriously: Jarl is fast and free!</p>
<p>And here are a few more reasons beyond our success stories <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f601.png" alt="😁" class="wp-smiley" style="height: 1em; max-height: 1em;" />:</p>
<h3 id="jarl-will-find-small-smelly-things">Jarl will find small smelly things</h3>
<p>For instance, the <a href="https://jarl.etiennebacher.com/rules/outer_negation" rel="nofollow" target="_blank"><code>outer_negation</code></a> rule helps improve readability: <code>!all(x)</code> is easier to understand than <code>any(!x)</code>. In igraph for example, thanks to that rule we <a href="https://github.com/igraph/rigraph/pull/2666" rel="nofollow" target="_blank">changed</a></p>
<pre>any(!names(options) %in% names(defaults))
</pre><p><em>Any names of options not in names of defaults</em> (and actually a tad worse since we do not use <code>%notin%</code> yet, new in <a href="https://cran.r-project.org/doc/manuals/r-release/NEWS.html" rel="nofollow" target="_blank">R 4.6.0</a>)</p>
<p>into</p>
<pre>!all(names(options) %in% names(defaults))
</pre><p><em>Not all names of options in names of defaults</em>.</p>
<p>See also the rule in action in <a href="https://github.com/tidymodels/parsnip/pull/1356/changes/8029bb8eb9acab8beef75975fd6675e38a70f802" rel="nofollow" target="_blank">parsnip</a>.</p>
<p>Generally, Jarl will make the codebase less <a href="https://github.com/jennybc/code-smells-and-feels" rel="nofollow" target="_blank">smelly</a>.</p>
<h3 id="jarl-will-find-unused-functions">Jarl will find unused functions</h3>
<p>It’s very easy for a package to accumulate functions that are now useless, sort of historical artefacts. And as they’re unused you might not even stumble upon them by chance. Jarl will flag them for you!</p>
<p>If Jarl notices such a function, check it is actually unused. If the function is actually useful, you might want to add an <a href="https://jarl.etiennebacher.com/howto/suppression-comments" rel="nofollow" target="_blank">exception comment</a> or some <a href="https://jarl.etiennebacher.com/reference/config-file#unused_function" rel="nofollow" target="_blank">rule-specific configuration</a>.</p>
<h3 id="jarl-will-find-duplicated-function-definitions">Jarl will find duplicated function definitions</h3>
<p>Also a problem that happens in a package that wasn’t born yesterday! In this case, you need to pick one definition. <a href="https://github.com/tidymodels/parsnip/issues/1360" rel="nofollow" target="_blank">Example in parsnip</a>. Deleting unused lines is very satisfying.</p>
<h3 id="jarl-will-detect-unreachable-code">Jarl will detect unreachable code</h3>
<p>For instance, code that comes after a <a href="https://rdrr.io/r/base/function.html" rel="nofollow" target="_blank"><code>return()</code></a> or a <a href="https://rdrr.io/r/base/stop.html" rel="nofollow" target="_blank"><code>stop()</code></a> in a function. Sometimes, you can simply delete that code (again, a feel good move!). Other times, the check is an opportunity for <a href="https://github.com/tidymodels/parsnip/issues/1359" rel="nofollow" target="_blank">targeted refactoring</a>!</p>
<h3 id="jarl-can-check-your-testthat-code">Jarl can check your testthat code</h3>
<p>As a bonus, because these rules are turned off by default, you can run</p>
<pre>jarl check . --select TESTTHAT
</pre>
<p>This will apply all rules from the <a href="https://jarl.etiennebacher.com/rules" rel="nofollow" target="_blank">testthat group</a> that help you use more specific expectations. For instance, <code>expect_equal(length(x), 2)</code> should be <code>expect_length(x, 2)</code>. All these rules come with automatic fixes!</p>
<p>Example in <a href="https://github.com/tidymodels/parsnip/pull/1379" rel="nofollow" target="_blank">parsnip</a>, example in <a href="https://github.com/igraph/rigraph/pull/2670" rel="nofollow" target="_blank">igraph</a>.</p>
<p>Should your package depend on dplyr, there’s a group of (currently two) rules for this as well!</p>
<h2 id="conclusion">Conclusion</h2>
<p>Automatic tools are extremely useful for guiding upkeep work. Thinking <em>“let me clean up this repo”</em> can be daunting <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f631.png" alt="😱" class="wp-smiley" style="height: 1em; max-height: 1em;" />, tools like Jarl provide a roadmap! They let you fix obvious problems, and at the same time, they can make you read some dusty corners of your codebase. Hannah’s work on parsnip inspired Maëlle to run Jarl on igraph (influencer influenced back!), and she too can confirm it was both useful and satisfying. Thanks Etienne for that cool tool!</p>
<p>Note that you could run Jarl checks manually as we did, or you could tell a LLM to run Jarl. To quote Hannah’s colleague <a href="https://emilhvitfeldt.com/post/ast-grep-r-claude-code/" rel="nofollow" target="_blank">Emil Hvitfeldt</a>, <em>“When coding or using coding agents, one way to improve your workflow is by using CLI tools with a very clear focus.”</em>. 🫡</p>
<p>What’s even better with Jarl is that it is actively developed, which you can confirm by skimming through the rich <a href="https://jarl.etiennebacher.com/dev/changelog" rel="nofollow" target="_blank">changelog</a>!</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>This inspired an <a href="https://github.com/etiennebacher/jarl/issues/497" rel="nofollow" target="_blank">issue</a>. As noted there, for this function one should add an <a href="https://jarl.etiennebacher.com/howto/suppression-comments" rel="nofollow" target="_blank">exception as a comment</a> or in the <a href="https://jarl.etiennebacher.com/reference/config-file#unused_function" rel="nofollow" target="_blank">config file</a>. <a href="https://blog.r-hub.io/2026/06/02/jarl/#fnref:1" class="footnote-backref" role="doc-backlink" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p>
</li>
</ol>
</div>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://blog.r-hub.io/2026/06/02/jarl/"> Posts on R-hub blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/06/refactoring-with-jarl-a-coffee-chat/">Refactoring with Jarl: a coffee chat</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401600</post-id>	</item>
		<item>
		<title>Our goodpractice Package Has New Superpowers</title>
		<link>https://www.r-bloggers.com/2026/05/our-goodpractice-package-has-new-superpowers/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Mon, 01 Jun 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/06/01/goodpractice/</guid>

					<description><![CDATA[<p>The goodpractice package has been recommended by rOpenSci since it was first started just over 10 years ago by Gábor Csárdi.<br />
We used to ask our editors to manually run goodpractice on all packages submitted to software peer-review, and then to ask aut...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/our-goodpractice-package-has-new-superpowers/">Our goodpractice Package Has New Superpowers</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://ropensci.org/blog/2026/06/01/goodpractice/"> rOpenSci - open tools for open science</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<p>The <a href="https://docs.ropensci.org/goodpractice" rel="nofollow" target="_blank">goodpractice package</a> has been recommended by rOpenSci since it was first started just over 10 years ago by <a href="https://gaborcsardi.org/" rel="nofollow" target="_blank">Gábor Csárdi</a>.
We used to ask our editors to manually run goodpractice on all packages submitted to software peer-review, and then to ask authors to fix any notable issues flagged by the package.
It is now integrated within our own <a href="https://docs.ropensci.org/pkgcheck" rel="nofollow" target="_blank">pkgcheck system</a>, and used to automatically identify any goodpractice issues with all new submissions.
The package changed maintainers <a href="https://ropensci.org/blog/2024/06/21/ropensci-news-june-2024/#ropensci-takes-over-maintenance-of-the-goodpractice-package" rel="nofollow" target="_blank">several times</a> before the previous maintainers gave the green light for us to take over maintenance of the package <a href="https://github.com/ropensci-review-tools/goodpractice/commit/c3f0b8e4c8e2cd7d88361bb670dd79284e6494d1" rel="nofollow" target="_blank">two years ago (28th May 2024)</a>.</p>
<p>We’re really pleased to share that we’ve recently rolled out a host of updates and extensions to the package.
These make it both easier to use, and more powerful.
This was a collaborative effort between new package author, Athanasia Mo Mowinckel, current maintainer, Mark Padgham, and the generative AI tool Claude.
We describe the process at the end of this tech note.</p>
<h2>
Easier control of checks
</h2><p>The goodpractice package is a convenience wrapper around several other packages including <a href="https://rcmdcheck.r-lib.org/" rel="nofollow" target="_blank">rcmdcheck</a>, <a href="https://lintr.r-lib.org/" rel="nofollow" target="_blank">lintr</a>, <a href="https://github.com/gaborcsardi/cyclocomp" rel="nofollow" target="_blank">cyclocomp</a>, and <a href="https://desc.r-lib.org/" rel="nofollow" target="_blank">desc</a>, along with a few hand-coded checks within the package itself.
The previous version of goodpractice had a total of 230 checks, generally prefixed with the name of the package which defined each check.
Checks were identifiable with the single function, <a href="https://docs.ropensci.org/goodpractice/reference/all_checks.html" rel="nofollow" target="_blank"><code>all_checks()</code></a>.
The only way to control which checks were run was to pass a <code>checks</code> parameter to the main function as a character vector of the names of checks you wanted to run.
So, for example, if you wanted to skip the <a href="https://rcmdcheck.r-lib.org/" rel="nofollow" target="_blank">rcmdcheck</a> checks, you had to do something like this:</p>
<pre>mychecks &lt;- grepv(&quot;^rcmdcheck&quot;, all_checks(), invert = TRUE)
gp(checks = mychecks)
</pre><p>That wasn’t easy!
And honestly, most people just didn’t bother.</p>
<p>This update finally makes it easier to control checks, by defining them in “groups”, which generally correspond to the separate packages that run the actual checks.
The groups can be seen with the new <a href="https://docs.ropensci.org/goodpractice/reference/all_check_groups.html" rel="nofollow" target="_blank"><code>all_check_groups()</code> function</a>, and controlled with the <a href="https://docs.ropensci.org/goodpractice/reference/checks_by_group.html" rel="nofollow" target="_blank"><code>checks_by_group()</code> function</a>.
Checks can be controlled by simply naming the check groups you want to run, like this:</p>
<pre>gp(checks = checks_by_group(&quot;description&quot;, &quot;lintr&quot;))
</pre><h2>
New check groups
</h2><p>The documentation for the <a href="https://docs.ropensci.org/goodpractice/reference/all_check_groups.html" rel="nofollow" target="_blank"><code>all_check_groups()</code> function</a> shows all the new check groups added in this update, including:</p>
<table>
<thead>
<tr>
<th>Group name</th>
<th>Run by default?</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>description</td>
<td>yes</td>
<td>Check common issues with <code>DESCRIPTION</code> files, including formatting issues with URLs, DOIs, package names, and author and contributor roles, along with other common <code>DESCRIPTION</code> issues.</td>
</tr>
<tr>
<td>rd</td>
<td>yes</td>
<td>Check whether or not <code>man/*.Rd</code> function documentation includes both example code and return values (regardless of whether or not documentation files are generated by <a href="https://roxygen2.r-lib.org/" rel="nofollow" target="_blank">roxygen2</a>).</td>
</tr>
<tr>
<td><a href="https://roxygen2.r-lib.org/" rel="nofollow" target="_blank">roxygen2</a></td>
<td>yes</td>
<td>Check for problems with Roxygen2 generated documentation.</td>
</tr>
<tr>
<td><a href="https://github.com/r-lib/revdepcheck" rel="nofollow" target="_blank">revdep</a></td>
<td>no</td>
<td>Reverse-dependency checks.</td>
</tr>
<tr>
<td>code_structure</td>
<td>yes</td>
<td>Checks for common issues like duplicated or unused functions.</td>
</tr>
<tr>
<td>package_structure</td>
<td>yes</td>
<td>Generic checks like whether a package has a README, a NEWS file, or whether all files use a <code>.R</code> extension, and not <code>.r</code>.</td>
</tr>
<tr>
<td><a href="https://docs.ropensci.org/spelling/" rel="nofollow" target="_blank">spelling</a></td>
<td>yes</td>
<td>Check spelling.</td>
</tr>
<tr>
<td><a href="https://tidyverse.org/" rel="nofollow" target="_blank">tidyverse</a></td>
<td>no</td>
<td>Check compliance with the <a href="https://style.tidyverse.org/" rel="nofollow" target="_blank"><em>Tidyverse style guide</em></a>, mostly <a href="https://lintr.r-lib.org/" rel="nofollow" target="_blank">lintr</a> checks.</td>
</tr>
<tr>
<td><a href="https://urlchecker.r-lib.org/" rel="nofollow" target="_blank">urlchecker</a></td>
<td>yes</td>
<td>Check whether all URLs are valid.</td>
</tr>
<tr>
<td>vignette</td>
<td>yes</td>
<td>Check that vignette code does not use either <code>rm()</code> or <code>setwd()</code>.</td>
</tr>
</tbody>
</table>
<p>Many of these checks are powered by <a href="https://ropensci.org/blog/2026/04/02/tree-sitter-overview/" rel="nofollow" target="_blank">treesitter</a>, the extremely efficient syntax parsing library used by GitHub — which is genuinely neat to have working in R.
The number of checks has been increased from 230 to 338, with all listed in the output of the <a href="https://docs.ropensci.org/goodpractice/reference/all_checks.html" rel="nofollow" target="_blank"><code>all_checks()</code> function</a>.</p>
<h2>
Improved check reporting
</h2><p>The goodpractice package has always been designed for console output.
This updated version now provides immediate and consistent detail on check progress while running.
The <a href="https://cli.r-lib.org/" rel="nofollow" target="_blank">cli package</a> is also used to give consistently formatted output for all check groups.</p>
<p>The large increase in checks also means that directly printing goodpractice results may fill several screens of output.
This update also adds a <code>groups</code> parameter to the <a href="https://docs.ropensci.org/goodpractice/reference/print.goodPractice.html" rel="nofollow" target="_blank"><code>print()</code> method</a>, to enable printing check results only for specified groups.
This means you can run the main <a href="https://docs.ropensci.org/goodpractice/reference/gp.html" rel="nofollow" target="_blank"><code>gp()</code> function</a> once, then step through each check group by printing results only for that group, fixing those, and then moving on to the next.
We’ve found this turns what could feel like an overwhelming wall of output into a manageable to-do list.
For example:</p>
<pre>x &lt;- gp()
print(x, &quot;description&quot;)
#&gt; Aww! Shining package! Keep up the priceless work!

print(x, &quot;namespace&quot;)
#&gt; ── It is good practice to ─────────────────────────────────────────────────────────────────────────────────────────────────────────────
#&gt;
#&gt; &#x2716; remove or use internal functions that are defined but never called. Dead code increases maintenance burden.
#&gt;
#&gt; R/utils.R:3
#&gt; R/utils.R:73
#&gt;
#&gt; &#x2716; define exported (user-facing) functions before internal helper functions within each R source file.
#&gt;
#&gt; R/api.R:85
#&gt;
#&gt; ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
</pre><hr>
<h2>
Notes on the development process
</h2><h3>
From Mo (Athanasia)
</h3><p>I’ll be honest with you: a lot of this contribution was done with an AI assistant (Claude Code using Opus 4.5) at my elbow.
I want to say something about that, because I think the honest version is more useful than the marketing version.
The way Mark approached reviewing this work was very thoughtful and helpful.</p>
<p>Based on his feedback and how it was structured, I’ve learned a lot about what a good code review looks like, and it’s been absolutely wonderful.
As a person who has mainly worked on their own code-bases alone, I have adopted a style of working that often creates diff monsters (hundreds of files changed), which can make reviewing very hard.
Indeed, my first PR to this project was such a monster, and Mark pushed back asking for several smaller PRs he could actually manage to review.
This is also where I learned about <a href="https://git-scm.com/docs/git-worktree" rel="nofollow" target="_blank">git worktrees</a> (Thank you <a href="https://masalmon.eu/2026/04/23/git-worktree-again/" rel="nofollow" target="_blank">Maëlle</a>!) and asking Claude to split the work up into several worktrees for easier review, was actually the first substantial change to how I now approach working with an AI assistant.
Through this collaboration I learned a lot about splitting my work into better sizes and chunks.</p>
<p>The shape of it was something like this:</p>
<p>I’d locate an issue I thought was meaningful to tackle, and provide Claude Code with the issue context and possibly an idea of how to solve it.
Once it had a solution, I’d review it, push back where it felt off, and we’d iterate.
Mark would then review the PR and point out the things I’d missed.
Three-way collaboration, more or less, with the AI doing the typing and me doing the deciding.</p>
<p>What worked well was scaffolding.
Drafting a new check, wiring up the prep step that goes with it, generating the test cases for the edge paths I’d otherwise forget — that kind of work compresses really nicely with an AI.
I would often ask it to generate some tests first, then create code that would pass the tests.
This way we had a clear idea of what we wanted the new code to do, and then solve it.</p>
<p>What worked less well was anything involving judgement I hadn’t articulated yet.
The first few times Claude opened a PR for me, it left the test-plan checkboxes unticked — even when the tests already passed.
That’s not wrong exactly, but it’s misleading to a reviewer.
I had to say “no, check the box if the test exists and passes; an unchecked box reads as TODO.”
Similarly, when Mark left inline code suggestions on a PR, Claude tried to helpfully re-implement them locally — which would have stripped Mark’s attribution off his own contribution.
We had to agree: suggestions get accepted on GitHub, not retyped.</p>
<p>And then there’s the unglamorous part.
It would often forget instructions, despite having them documented in agents.md and local memory for the project.
These were mostly trivial, but at times fairly bad.
Some of the first PRs we made, Mark pointed out that the solutions were using regexp rather than AST (Abstrat Syntax Trees).
I have to admit, I didn’t really know about AST before starting this project, and I am so glad I now know of it.
Claude, despite being told very clearly that we wanted AST solutions, would often forget and implement regexp based solutions.
It would also often revert to using for-loops rather than vectorization, and creating nested for-loops into the 3rd or 4th level — which is just horrible to follow as a human.</p>
<p>If you take one thing from this aside, take this: AI-assisted contribution didn’t mean handing the package over to a machine.
It meant I could move faster on the parts I already understood, explore more confidently on the parts I didn’t, and spend my actual attention on the decisions that mattered — what to check for, how to group it, what to call it, and what to leave out.
The friction taught me where to watch closely.
The speed-ups taught me where I could trust the loop.</p>
<p>It’s a different way of working, and I’m still figuring out the shape of it, but it has enabled me to contribute to this project despite some very severe health issues making it hard for me to work as normal.</p>
<p>And one thing Claude always does better than me: writing good commit messages.
I have found myself writing my own code, but asking Claude to commit them (making it look like Claude wrote the code, but I don’t care) because the commit messages are just so much better than what I write.</p>
<h3>
From Mark
</h3><p>The entire idea for this major update was initiated and driven by Mo.
It was also my first real foray into using generative AI tools directly in rOpenSci packages, and I learnt a lot.
After I rejected the initial monster PR, things settled down into much smaller and manageable PRs that were always focussed on one specific thing.</p>
<p>From that point, Mo was effectively the coder (assisted by Claude), and I was the reviewer.
I never knew how much of the actual code was typed versus machine-generated, but was generally pleasantly surprised that I felt no need to inquire.
Nearly every PR was obviously going to improve the package, and many of them were genuinely creative ideas.
(That’s where I disagree slightly with Mo’s general description of the process: I am convinced a lot of the work behind this update came directly from her brilliant insights and experience, rather than her passive description of looking over pre-existing PRs.)</p>
<p>The process started in Feb 2026, and involved <a href="https://github.com/ropensci-review-tools/goodpractice/pulls?q=sort%3Aupdated-desc%20is%3Apr%20is%3Aclosed%20author%3Adrmowinckels" rel="nofollow" target="_blank">70 pull requests</a>.
So many of these really were independent and new ideas.
That enabled me to view each one with fresh eyes, and to think about whether I might approach anything in different ways.
Over so many PRs, there was a lot of back-and-forth, from pushing back on Claude insisting on nested loops, to figuring out whether code was best parsed and analysed as text, or as a syntax tree.
As Mo indicated, a lot of those discussions ultimately converged towards us bringing the power of treesitter into the package, and even helping to replace previous text-based code checks with more efficient and accurate AST approaches.</p>
<p>Our collaboration was very pleasant and enriching, and really felt like a direct cooperation between Mo as the ideas factory, me as the reviewer, and Claude as nothing more than a mediator of our ideas.
The key role of Claude throughout the process was in handling all of the fiddly, technical details.
We both looked carefully at every single line of code, but the use of Claude enabled our conversations to remain at higher, conceptual levels than what would have happened if we had to actually do all the technical implementation ourselves.
I think that is the aspect that I found most surprising: That the use of Claude made our collaboration feel less technical, and therefore somehow even more human.
And that gave us the ability to work though 70 pull requests representing over 100 new checks, all ready for everybody to use.</p>
<h2>
Let us know what you think
</h2><p>Like all rOpenSci packages, goodpractice is a community effort that lives through community use and feedback.
We’d love to hear what you think, via <a href="mailto:mark@ropensci.org" rel="nofollow" target="_blank">email</a>, or through issues on the <a href="https://github.com/ropensci-review-tools/goodpractice" rel="nofollow" target="_blank">GitHub repository</a>.</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://ropensci.org/blog/2026/06/01/goodpractice/"> rOpenSci - open tools for open science</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/our-goodpractice-package-has-new-superpowers/">Our goodpractice Package Has New Superpowers</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401573</post-id>	</item>
		<item>
		<title>Techtonique dot net, the Machine Learning web API, is back online (but more like a passion project for now)</title>
		<link>https://www.r-bloggers.com/2026/05/techtonique-dot-net-the-machine-learning-web-api-is-back-online-but-more-like-a-passion-project-for-now/</link>
		
		<dc:creator><![CDATA[T. Moudiki]]></dc:creator>
		<pubDate>Sun, 31 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://thierrymoudiki.github.io//blog/2026/05/31/r/python/techtonique/techtonique-dot-net-is-back</guid>

					<description><![CDATA[<p>Techtonique dot net is back, but more like a passion project, with an API for machine learning tasks (classification, regression, survival analysis, reserving, forecasting etc.). Examples in R, Python are provided in the blog post.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/techtonique-dot-net-the-machine-learning-web-api-is-back-online-but-more-like-a-passion-project-for-now/">Techtonique dot net, the Machine Learning web API, is back online (but more like a passion project for now)</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://thierrymoudiki.github.io//blog/2026/05/31/r/python/techtonique/techtonique-dot-net-is-back"> T. Moudiki's Webpage - R</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p><a href="https://www.techtonique.net/" rel="nofollow" target="_blank">https://www.techtonique.net</a> contains tools for <strong>Exploratory Data Analysis</strong> (EDA), <strong>editors</strong> for R and Python code, tools for <strong>data visualization</strong>, <strong>no-code web interfaces</strong> for various data science tasks, <strong>a language-agnostic API</strong> for machine learning tasks (classification, regression, survival analysis, reserving, forecasting etc.).</p>

<p>I recently noticed a <strong>spike</strong> in sign-ups (not sure how they noticed it was up again, without official announcement…), and I thought it was a good opportunity to share that <strong><a href="https://www.techtonique.net/" rel="nofollow" target="_blank">techtonique.net</a> is back</strong>. More like a passion project, or part of a portfolio (for now). The API is available and evolving. You can use it for free (with rate limiting), but please note that it is slightly slower than it used to be (indeed, I used to have Azure Credits thanks to Microsoft for Startups  => a faster server). It still serves the responses, in a reasonable time though, and I will try to optimize it as much as possible.</p>

<p>It is worth mentioning that I removed the stochastic simulation API for now, as it required (more technical) to run R in Python, through a Docker container.</p>

<p>Here are some examples of how to use the API for machine learning tasks in R and Python (there are also no-code interfaces for these tasks in the website, and the API is language-agnostic, so you can use it with any programming language that can make HTTP requests):</p>

<p>The starting point is to signup/login (if you’re facing issues, contact support@techtonique.net) and get a token from: <a href="https://www.techtonique.net/token" rel="nofollow" target="_blank">https://www.techtonique.net/token</a>. Then, you can use the token in API requests for machine learning tasks. Each response is a prediction, as envisaged by the chosen model, and based on the input data.</p>

<h1 id="1---r-example">1 &#8211; R example:</h1>

<pre>#!/usr/bin/env Rscript

# Install httr if needed: install.packages(&quot;httr&quot;)
# All you need is a token from www.techtonique.net/token, then run the script
library(httr)

BASE_URL   &lt;- &quot;https://www.techtonique.net&quot;
GITHUB_RAW &lt;- &quot;https://raw.githubusercontent.com/Techtonique/datasets/main&quot;

DATASETS &lt;- list(
  univariate     = paste0(GITHUB_RAW, &quot;/time_series/univariate/a10.csv&quot;),
  multivariate   = paste0(GITHUB_RAW, &quot;/time_series/multivariate/ice_cream_vs_heater.csv&quot;),
  classification = paste0(GITHUB_RAW, &quot;/tabular/classification/breast_cancer_dataset2.csv&quot;),
  regression     = paste0(GITHUB_RAW, &quot;/tabular/regression/boston_dataset2.csv&quot;),
  raa            = paste0(GITHUB_RAW, &quot;/tabular/triangle/raa.csv&quot;),
  abc            = paste0(GITHUB_RAW, &quot;/tabular/triangle/abc.csv&quot;),
  km             = paste0(GITHUB_RAW, &quot;/tabular/survival/kidney.csv&quot;),
  ridge_survival = paste0(GITHUB_RAW, &quot;/tabular/survival/gbsg2_2.csv&quot;)
)

# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

get_token &lt;- function() {
  args &lt;- commandArgs(trailingOnly = TRUE)
  idx  &lt;- which(args == &quot;--token&quot;)
  if (length(idx) &gt; 0 && idx &lt; length(args)) return(args[idx + 1])
  cat(&quot;Please enter your JWT token: &quot;)
  readLines(con = &quot;stdin&quot;, n = 1)
}

# Download a GitHub dataset to a temp file and return its path.
# Using a real file is the only reliable way to do multipart uploads in httr.
fetch_dataset &lt;- function(key) {
  url      &lt;- DATASETS[[key]]
  filename &lt;- basename(url)
  cat(sprintf(&quot;  Fetching %s from GitHub... &quot;, filename))
  tmp &lt;- tempfile(fileext = &quot;.csv&quot;)
  resp &lt;- GET(url, write_disk(tmp, overwrite = TRUE))
  stop_for_status(resp)
  cat(&quot;OK\n&quot;)
  tmp   # return the temp file path
}

make_request &lt;- function(endpoint, token, dataset_key = NULL, params = list()) {
  url     &lt;- paste0(BASE_URL, endpoint)
  headers &lt;- add_headers(Authorization = paste(&quot;Bearer&quot;, token))

  if (!is.null(dataset_key)) {
    tmp_path &lt;- fetch_dataset(dataset_key)
    on.exit(unlink(tmp_path), add = TRUE)   # clean up temp file when done
    body &lt;- list(file = upload_file(tmp_path, type = &quot;text/csv&quot;))
    resp &lt;- POST(url, headers, query = params, body = body, encode = &quot;multipart&quot;)
  } else {
    resp &lt;- GET(url, headers, query = params)
  }

  if (http_error(resp)) {
    cat(sprintf(&quot;  ERROR %s: %s\n&quot;, status_code(resp),
                content(resp, as = &quot;text&quot;, encoding = &quot;UTF-8&quot;)))
    return(NULL)
  }
  content(resp, as = &quot;parsed&quot;, type = &quot;application/json&quot;)
}

print_response &lt;- function(resp) {
  if (is.null(resp)) return(invisible(NULL))
  for (key in names(resp)) {
    val &lt;- resp[[key]]
    if (is.list(val) || length(val) &gt; 6) {
      cat(sprintf(&quot;  %s: [%s ...]\n&quot;, key, paste(head(unlist(val), 6), collapse = &quot;, &quot;)))
    } else {
      cat(sprintf(&quot;  %s: %s\n&quot;, key, paste(val, collapse = &quot;, &quot;)))
    }
  }
}

# ---------------------------------------------------------------------------
# Test sections
# ---------------------------------------------------------------------------

test_forecasting &lt;- function(token) {
  cat(&quot;\n=== Testing Forecasting Endpoints ===\n&quot;)

  cat(&quot;\nTesting univariate forecasting...\n&quot;)
  resp &lt;- make_request(&quot;/forecasting&quot;, token,
    dataset_key = &quot;univariate&quot;,
    params = list(base_model = &quot;RidgeCV&quot;, n_hidden_features = 5,
                  lags = 25, type_pi = &quot;kde&quot;, replications = 4, h = 3))
  print_response(resp)

  cat(&quot;\nTesting multivariate forecasting...\n&quot;)
  resp &lt;- make_request(&quot;/forecasting&quot;, token,
    dataset_key = &quot;multivariate&quot;,
    params = list(base_model = &quot;RidgeCV&quot;, n_hidden_features = 5, lags = 25, h = 3))
  print_response(resp)
}

test_ml &lt;- function(token) {
  cat(&quot;\n=== Testing Machine Learning Endpoints ===\n&quot;)

  cat(&quot;\nTesting classification...\n&quot;)
  resp &lt;- make_request(&quot;/mlclassification&quot;, token,
    dataset_key = &quot;classification&quot;,
    params = list(base_model = &quot;RandomForestClassifier&quot;,
                  n_hidden_features = 5, predict_proba = TRUE))
  print_response(resp)

  cat(&quot;\nTesting regression...\n&quot;)
  resp &lt;- make_request(&quot;/mlregression&quot;, token,
    dataset_key = &quot;regression&quot;,
    params = list(base_model = &quot;RidgeCV&quot;, n_hidden_features = 5, return_pi = TRUE))
  print_response(resp)
}

test_reserving &lt;- function(token) {
  cat(&quot;\n=== Testing Reserving Endpoints ===\n&quot;)

  cat(&quot;\nTesting RidgeCV...\n&quot;)
  resp &lt;- make_request(&quot;/mlreserving&quot;, token,
    dataset_key = &quot;raa&quot;,
    params = list(method = &quot;RidgeCV&quot;))
  print_response(resp)

  cat(&quot;\nTesting LassoCV...\n&quot;)
  resp &lt;- make_request(&quot;/mlreserving&quot;, token,
    dataset_key = &quot;abc&quot;,
    params = list(method = &quot;LassoCV&quot;))
  print_response(resp)
}

test_survival &lt;- function(token) {
  cat(&quot;\n=== Testing Survival Analysis Endpoints ===\n&quot;)

  cat(&quot;\nTesting Kaplan-Meier...\n&quot;)
  resp &lt;- make_request(&quot;/survivalcurve&quot;, token,
    dataset_key = &quot;km&quot;,
    params = list(method = &quot;km&quot;))
  print_response(resp)

  cat(&quot;\nTesting Ridge survival...\n&quot;)
  resp &lt;- make_request(&quot;/survivalcurve&quot;, token,
    dataset_key = &quot;ridge_survival&quot;,
    params = list(method = &quot;RidgeCV&quot;, patient_id = 0))
  print_response(resp)
}

# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------

main &lt;- function() {
  token &lt;- get_token()
  test_forecasting(token)
  test_ml(token)
  test_reserving(token)
  test_survival(token)
}

main()
</pre>

<h1 id="2---python-example">2 - Python example:</h1>

<pre>import argparse
import requests
import os
import io
from typing import Dict, Any

# All you need is a token from www.techtonique.net/token, then run the script

# Base URL for API endpoints
base_url = &quot;https://www.techtonique.net&quot;

# Base URL for raw GitHub content
GITHUB_RAW = &quot;https://raw.githubusercontent.com/Techtonique/datasets/main&quot;

# Dataset paths within the GitHub repo
DATASETS = {
    &quot;univariate&quot;:       f&quot;{GITHUB_RAW}/time_series/univariate/a10.csv&quot;,
    &quot;multivariate&quot;:     f&quot;{GITHUB_RAW}/time_series/multivariate/ice_cream_vs_heater.csv&quot;,
    &quot;classification&quot;:   f&quot;{GITHUB_RAW}/tabular/classification/breast_cancer_dataset2.csv&quot;,
    &quot;regression&quot;:       f&quot;{GITHUB_RAW}/tabular/regression/boston_dataset2.csv&quot;,
    &quot;raa&quot;:              f&quot;{GITHUB_RAW}/tabular/triangle/raa.csv&quot;,
    &quot;abc&quot;:              f&quot;{GITHUB_RAW}/tabular/triangle/abc.csv&quot;,
    &quot;km&quot;:               f&quot;{GITHUB_RAW}/tabular/survival/kidney.csv&quot;,
    &quot;ridge_survival&quot;:   f&quot;{GITHUB_RAW}/tabular/survival/gbsg2_2.csv&quot;,
}


def get_token() -&gt; str:
    &quot;&quot;&quot;Get token from command line argument or prompt&quot;&quot;&quot;
    parser = argparse.ArgumentParser(description='Test API endpoints')
    parser.add_argument('--token', help='JWT token for authentication')
    args = parser.parse_args()

    if args.token:
        return args.token

    return input(&quot;Please enter your JWT token: &quot;)


def fetch_dataset(dataset_key: str) -&gt; tuple[io.BytesIO, str]:
    &quot;&quot;&quot;
    Download a dataset from GitHub and return it as an in-memory bytes buffer
    together with a filename, ready to be passed to requests as a file upload.
    &quot;&quot;&quot;
    url = DATASETS[dataset_key]
    filename = url.split(&quot;/&quot;)[-1]
    print(f&quot;  Fetching {filename} from GitHub...&quot;, end=&quot; &quot;)
    response = requests.get(url)
    response.raise_for_status()
    print(&quot;OK&quot;)
    return io.BytesIO(response.content), filename


def make_request(
    url: str,
    token: str,
    method: str = &quot;POST&quot;,
    file_tuple: tuple = None,   # (BytesIO, filename)
    params: Dict = None,
) -&gt; Dict[str, Any]:
    &quot;&quot;&quot;Make an API request and return the response.&quot;&quot;&quot;
    headers = {&quot;Authorization&quot;: f&quot;Bearer {token}&quot;}

    try:
        if method == &quot;POST&quot;:
            files = None
            if file_tuple:
                buf, filename = file_tuple
                files = {&quot;file&quot;: (filename, buf, &quot;text/csv&quot;)}
            response = requests.post(url, headers=headers, files=files, params=params)
        else:
            response = requests.get(url, headers=headers, params=params)

        response.raise_for_status()
        return response.json()

    except requests.exceptions.RequestException as e:
        print(f&quot;Error making request to {url}:&quot;)
        print(f&quot;Status code: {e.response.status_code if hasattr(e, 'response') else 'N/A'}&quot;)
        print(f&quot;Response: {e.response.text if hasattr(e, 'response') else str(e)}&quot;)
        return None


def test_forecasting(token: str):
    print(&quot;\n=== Testing Forecasting Endpoints ===&quot;)

    print(&quot;\nTesting univariate forecasting...&quot;)
    response = make_request(
        f&quot;{base_url}/forecasting&quot;, token,
        file_tuple=fetch_dataset(&quot;univariate&quot;),
        params={&quot;base_model&quot;: &quot;RidgeCV&quot;, &quot;n_hidden_features&quot;: 5,
                &quot;lags&quot;: 25, &quot;type_pi&quot;: &quot;kde&quot;, &quot;replications&quot;: 4, &quot;h&quot;: 3},
    )
    if response:
        print(&quot;Response:&quot;, response)

    print(&quot;\nTesting multivariate forecasting...&quot;)
    response = make_request(
        f&quot;{base_url}/forecasting&quot;, token,
        file_tuple=fetch_dataset(&quot;multivariate&quot;),
        params={&quot;base_model&quot;: &quot;RidgeCV&quot;, &quot;n_hidden_features&quot;: 5, &quot;lags&quot;: 25, &quot;h&quot;: 3},
    )
    if response:
        print(&quot;Response:&quot;, response)


def test_ml(token: str):
    print(&quot;\n=== Testing Machine Learning Endpoints ===&quot;)

    print(&quot;\nTesting classification...&quot;)
    response = make_request(
        f&quot;{base_url}/mlclassification&quot;, token,
        file_tuple=fetch_dataset(&quot;classification&quot;),
        params={&quot;base_model&quot;: &quot;RandomForestClassifier&quot;,
                &quot;n_hidden_features&quot;: 5, &quot;predict_proba&quot;: True},
    )
    if response:
        print(&quot;Response:&quot;, response)

    print(&quot;\nTesting regression...&quot;)
    response = make_request(
        f&quot;{base_url}/mlregression&quot;, token,
        file_tuple=fetch_dataset(&quot;regression&quot;),
        params={&quot;base_model&quot;: &quot;RidgeCV&quot;, &quot;n_hidden_features&quot;: 5, &quot;return_pi&quot;: True},
    )
    if response:
        print(&quot;Response:&quot;, response)


def test_reserving(token: str):
    print(&quot;\n=== Testing Reserving Endpoints ===&quot;)

    print(&quot;\nTesting RidgeCV...&quot;)
    response = make_request(
        f&quot;{base_url}/mlreserving&quot;, token,
        file_tuple=fetch_dataset(&quot;raa&quot;),
        params={&quot;method&quot;: &quot;RidgeCV&quot;},
    )
    if response:
        print(&quot;Response:&quot;, response)

    print(&quot;\nTesting LassoCV...&quot;)
    response = make_request(
        f&quot;{base_url}/mlreserving&quot;, token,
        file_tuple=fetch_dataset(&quot;abc&quot;),
        params={&quot;method&quot;: &quot;LassoCV&quot;},
    )
    if response:
        print(&quot;Response:&quot;, response)


def test_survival(token: str):
    print(&quot;\n=== Testing Survival Analysis Endpoints ===&quot;)

    print(&quot;\nTesting Kaplan-Meier...&quot;)
    response = make_request(
        f&quot;{base_url}/survivalcurve&quot;, token,
        file_tuple=fetch_dataset(&quot;km&quot;),
        params={&quot;method&quot;: &quot;km&quot;},
    )
    if response:
        print(&quot;Response:&quot;, response)

    print(&quot;\nTesting Ridge survival...&quot;)
    response = make_request(
        f&quot;{base_url}/survivalcurve&quot;, token,
        file_tuple=fetch_dataset(&quot;ridge_survival&quot;),
        params={&quot;method&quot;: &quot;RidgeCV&quot;, &quot;patient_id&quot;: 0},
    )
    if response:
        print(&quot;Response:&quot;, response)


def main():
    token = get_token()
    test_forecasting(token)
    test_ml(token)
    test_reserving(token)
    test_survival(token)


if __name__ == &quot;__main__&quot;:
    main()
</pre>

<p><img src="https://i2.wp.com/thierrymoudiki.github.io/images/2025-06-09/2025-06-09-image1.gif?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://thierrymoudiki.github.io//blog/2026/05/31/r/python/techtonique/techtonique-dot-net-is-back"> T. Moudiki's Webpage - R</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/techtonique-dot-net-the-machine-learning-web-api-is-back-online-but-more-like-a-passion-project-for-now/">Techtonique dot net, the Machine Learning web API, is back online (but more like a passion project for now)</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401561</post-id>	</item>
		<item>
		<title>April 2026 Top 40 New CRAN Packages</title>
		<link>https://www.r-bloggers.com/2026/05/april-2026-top-40-new-cran-packages/</link>
		
		<dc:creator><![CDATA[Joseph Rickert]]></dc:creator>
		<pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/april-2026-top-40-new-cran-packages/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Three hundred seventy-six of the new packages submitted to CRAN in April were still there in mid-May. Here are my Top 40 picks in twenty-three categories: Actuarial Analysis, Archaeology, Biology, Causal Inference, Computational Methods, Ecology...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/april-2026-top-40-new-cran-packages/">April 2026 Top 40 New CRAN Packages</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/april-2026-top-40-new-cran-packages/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>Three hundred seventy-six of the new packages submitted to CRAN in April were still there in mid-May. Here are my Top 40 picks in twenty-three categories: Actuarial Analysis, Archaeology, Biology, Causal Inference, Computational Methods, Ecology, Economics, Environmental Studies, Epidemiology, Finance, Functional Data Analysis, Health Technology Assessment, Machine Learning, Medical Statistics, Meta Analysis, Networks, Physics, Programming, Statistics, Time Series, Utilities, and Visualization.</p>
<div class="columns">
<div class="column" style="width:45%;">
<section id="actuarial-analysis" class="level3">
<h3 class="anchored" data-anchor-id="actuarial-analysis">Actuarial Analysis</h3>
<p><a href="https://cran.r-project.org/package=mqriskR" rel="nofollow" target="_blank">mqriskR</a> v0.1.0: Provides functions for actuarial risk modeling, including survival models, life annuities, multiple-decrement models, and mortality improvement projections. The package is designed to align with standard actuarial notation and supports teaching, exam preparation, and reproducible actuarial analysis. The methods are based on standard actuarial references, including <a href="https://www.amazon.com/Models-Quantifying-Risk-Richard-London/dp/1625423470" rel="nofollow" target="_blank">Camilli, Duncan and London (2014)</a>, and <a href="https://www.amazon.com/Actuarial-Mathematics-Contingent-International-Science/dp/1108478085/ref=sr_1_1?dib=eyJ2IjoiMSJ9.o7GXBw5H5JYFDcvNeOrpM58_PZqcW-2pMEeMTdCpcmxkl8sSFgx9YEBiBMmqr-wWFWZPgpoWKfjvwnvE7ASss2GsLU84aE3P7YsnfFYbBbBBtlfbK0fTcwWnH23XKnJwV6JkxJMocNt2MQdIyCmdDsqWBHNyRILKd17PUpOZyPnMWkBsXn9kXX2qedo98iumU6HQiObaIzG8nmpm3U-RYVgnomDlUWq61OnCS37EDMc.Lb6GlPsG2422vJHe1j-GwV_1RSSDvznlhHe7cbX-g2I&#038;dib_tag=se&#038;keywords=Actuarial+Mathematics+for+Life+Contingent+Risks&#038;qid=1779844083&#038;s=books&#038;sr=1-1" rel="nofollow" target="_blank">Dickson, Hardy and Waters (2020)</a>. See the <a href="https://cran.r-project.org/web/packages/mqriskR/vignettes/getting-started.html" rel="nofollow" target="_blank">vignette</a>.</p>
</section>
<section id="archaeology" class="level3">
<h3 class="anchored" data-anchor-id="archaeology">Archaeology</h3>
<p><a href="https://cran.r-project.org/package=palimpsestr" rel="nofollow" target="_blank">palimpsestr</a> v0.10.0: Implements a probabilistic framework for the analysis of archaeological palimpsests based on the Stratigraphic Entanglement Field which integrates spatial proximity, stratigraphic depth, chronological overlap, and cultural similarity to estimate latent depositional phases via diagonal Gaussian mixture Expectation-Maximisation. Includes simulation, diagnostics, phase-count selection, publication-quality plots, and Geographic Information System export via <code>sf</code>. Methods are described in <a href="https://zenodo.org/records/19969277" rel="nofollow" target="_blank">Cocca (2026)</a>. See the <a href="https://cran.r-project.org/web/packages/palimpsestr/vignettes/introduction.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/palimpsestr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-1" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/palimpsestr.png?w=578&#038;ssl=1" class="img-fluid" alt="Vertical Phase Profile Plot" data-recalc-dims="1"></a></p>
</section>
<section id="biology" class="level3">
<h3 class="anchored" data-anchor-id="biology">Biology</h3>
<p><a href="https://cran.r-project.org/package=bsocialv2" rel="nofollow" target="_blank">bsocialv2</a> v0.2.1: Provides an S4 class and methods for analyzing microbial social behavior in bacterial consortia. Includes growth parameter extraction, social behavior classification (cooperators/cheaters/neutrals), diversity effect analysis, consortium assembly path finding, and stability analysis via coefficient of variation. Methods are described in <a href="https://www.frontiersin.org/journals/microbiology/articles/10.3389/fmicb.2017.00919/full" rel="nofollow" target="_blank">Purswani et al. (2017)</a>. See the <a href="https://cran.r-project.org/web/packages/bsocialv2/vignettes/bsocial-workflow.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/bsocialv2.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-2" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/bsocialv2.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot showing fitness over number of generations" data-recalc-dims="1"></a></p>
</section>
<section id="causal-inference" class="level3">
<h3 class="anchored" data-anchor-id="causal-inference">Causal Inference</h3>
<p><a href="https://cran.r-project.org/package=CausalMixGPD" rel="nofollow" target="_blank">CausalMixGPD</a> v0.8.0: Implements tools for Bayesian analysis of heavy-tailed outcomes by combining Dirichlet process mixture models for the body of the distribution with optional generalized Pareto tails. The method allows for unconditional and covariate-modulated mixtures, implements MCMC estimation using <code>nimble</code>, and extends to mixtures of different arms’ outcomes with application to causal inference in the <a href="https://psycnet.apa.org/doiLanding?doi=10.1037%2Fh0037350" rel="nofollow" target="_blank">Rubin (1974)</a> framework. There are three vignettes, including an <a href="https://cran.r-project.org/web/packages/CausalMixGPD/vignettes/cmgpd_causal.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/CausalMixGPD/vignettes/cmgpd_one_arm.html" rel="nofollow" target="_blank">One-Arm Regression Modeling</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/CausalMixGPD.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-3" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/CausalMixGPD.png?w=578&#038;ssl=1" class="img-fluid" alt="Quantile predictions with pointwise credible intervals" data-recalc-dims="1"></a></p>
</section>
<section id="computational-methods" class="level3">
<h3 class="anchored" data-anchor-id="computational-methods">Computational Methods</h3>
<p><a href="https://cran.r-project.org/package=nmfkc" rel="nofollow" target="_blank">nmfkc</a> v0.7.3: Provides functions to perform non-negative matrix factorization with kernel covariates. Given an observation matrix and kernel covariates, it optimizes both a basis matrix and a parameter matrix. Also provides NMF with random effects, which estimates a mixed-effects model combining covariate-driven scores with unit-specific random effects together with wild bootstrap inference, and NMF-based structural equation modeling. See <a href="https://arxiv.org/abs/2403.05359" rel="nofollow" target="_blank">Satoh (2025)</a> and <a href="https://link.springer.com/article/10.1007/s42081-025-00314-0" rel="nofollow" target="_blank">Satoh (2026)</a> for background. There are six vignettes, including <a href="https://cran.r-project.org/web/packages/nmfkc/vignettes/introduction-to-nmfkc.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/nmfkc/vignettes/topic-modeling-with-nmfkc.html" rel="nofollow" target="_blank">Topic Modeling</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/nmfkc.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-4" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/nmfkc.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of topic proportions" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=symbolicr" rel="nofollow" target="_blank">symbolicr</a> v1.0.0: Provides functions to find non-linear formulas that fit input data. Users can systematically explore and memorize the possible formulas and their cross-validation performance, in an incremental fashion. Three main interoperable search functions are available: 1) <code>random.search()</code> performs a random exploration, 2) <code>genetic.search()</code> employs a genetic optimization algorithm, 3) <code>comb.search()</code> combines the best results of the first two. For more details, see <a href="https://link.springer.com/article/10.1208/s12248-026-01232-z" rel="nofollow" target="_blank">Tomasoni et al. (2026)</a>. There are three vignettes, including <a href="https://cran.r-project.org/web/packages/symbolicr/vignettes/get-started.html" rel="nofollow" target="_blank">get-started</a> and <a href="https://cran.r-project.org/web/packages/symbolicr/vignettes/formula-analysis.html" rel="nofollow" target="_blank">formula-analysis</a>.</p>
</section>
<section id="ecology" class="level3">
<h3 class="anchored" data-anchor-id="ecology">Ecology</h3>
<p><a href="https://cran.r-project.org/package=CharAnalysis" rel="nofollow" target="_blank">CharAnalysis</a> v2.0.3: Implements a program for reconstructing local fire histories from high-resolution, continuously sampled lake-sediment charcoal records. Functions decompose a charcoal record into low- and high-frequency components and use locally defined thresholds to separate fire signal from noise. See <a href="https://esajournals.onlinelibrary.wiley.com/doi/10.1890/07-2019.1" rel="nofollow" target="_blank">Higuera et al. (2009)</a> and <a href="https://connectsci.au/wf/article-abstract/19/8/996/23290/Peak-detection-in-sediment-charcoal-records?redirectedFrom=fulltext" rel="nofollow" target="_blank">Higuera et al. (2010)</a> for background and the <a href="https://cran.r-project.org/web/packages/CharAnalysis/vignettes/CharAnalysis_intro.html" rel="nofollow" target="_blank">vignette</a> to get started.</p>
<p><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/CharAnalysis.png?w=578&#038;ssl=1" class="img-fluid" alt="Each panel showing the empirical Cpeak distribution within that window, the fitted noise component (Gaussian or Gaussian mixture, per threshMethod), and the resulting threshold value at the working percentile" data-recalc-dims="1"> <a href="https://cran.r-project.org/package=SQIpro" rel="nofollow" target="_blank">SQIpro</a> v0.1.0: Provides a comprehensive, modular framework for computing the Soil Quality Index (SQI) using six established methods: Linear Scoring <a href="https://acsess.onlinelibrary.wiley.com/doi/10.2136/sssaspecpub35.c1" rel="nofollow" target="_blank">Doran and Parkin, (1994)</a>. Regression-based <a href="https://link.springer.com/article/10.1007/s10661-007-9697-z" rel="nofollow" target="_blank">Masto et al. (2008)</a>, Principal Component Analysis <a href="https://acsess.onlinelibrary.wiley.com/doi/10.2136/sssaj2004.1945" rel="nofollow" target="_blank">Andrews et al. (2004)</a>, Fuzzy Logic, Entropy Weighting, TOPSIS <a href="https://link.springer.com/book/10.1007/978-3-642-48318-9" rel="nofollow" target="_blank">Hwang and Yoon (1981)</a>. See the <a href="https://cran.r-project.org/web/packages/SQIpro/vignettes/getting_started.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/SQIpro.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-5" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/SQIpro.png?w=578&#038;ssl=1" class="img-fluid" alt="PCA Biplot of Soil Quality Variables" data-recalc-dims="1"></a></p>
</section>
<section id="economics" class="level3">
<h3 class="anchored" data-anchor-id="economics">Economics</h3>
<p><a href="https://cran.r-project.org/package=rescomp" rel="nofollow" target="_blank">rescomp</a> v1.0.0: Provides functions to generate, simulate, and visualize ODE models of consumer-resource interactions and competition modeling. There is an <a href="https://cran.r-project.org/web/packages/rescomp/vignettes/rescomp.html" rel="nofollow" target="_blank">Introduction</a> and a <a href="https://cran.r-project.org/web/packages/rescomp/vignettes/classic-results.html" rel="nofollow" target="_blank">vignette</a> reproducing classic results.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/rescomp.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-6" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/rescomp.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots showing behavior of three 'type 1' comusers on three essential resources over time" data-recalc-dims="1"></a></p>
</section>
<section id="environmental-studies" class="level3">
<h3 class="anchored" data-anchor-id="environmental-studies">Environmental Studies</h3>
<p><a href="https://cran.r-project.org/package=gleam" rel="nofollow" target="_blank">gleam</a> v0.8.0: This official implementation of the Global Livestock Environmental Assessment Model of the Food and Agriculture Organization of the United Nations (<a href="https://www.fao.org/gleam/en/" rel="nofollow" target="_blank">GLEAM</a>) provides a modular, transparent framework for simulating livestock production systems and quantifying their environmental impacts. See <a href="https://www.sciencedirect.com/science/article/pii/S1751731117001847" rel="nofollow" target="_blank">MacLeod et al. (2017)</a> for background. There are four vignettes, including an <a href="https://cran.r-project.org/web/packages/gleam/vignettes/gleam-overview.html" rel="nofollow" target="_blank">Overview</a> and <a href="https://cran.r-project.org/web/packages/gleam/vignettes/gleam-modules-overview.html" rel="nofollow" target="_blank">Package Modules</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/gleam.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-7" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/gleam.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots showing behavior of three 'type 1' comusers on three essential resources over time" data-recalc-dims="1"></a></p>
</section>
<section id="epidemiology" class="level3">
<h3 class="anchored" data-anchor-id="epidemiology">Epidemiology</h3>
<p><a href="https://cran.r-project.org/package=lineagefreq" rel="nofollow" target="_blank">lineagefreq</a> v0.2.0: Provides functions to model pathogen lineage frequency dynamics from genomic surveillance count data. Includes a unified interface for multinomial logistic regression, hierarchical partial-pooling models, the Piantham approximation for relative reproduction number estimation, and features such as rolling-origin backtesting, standardized forecast scoring. See <a href="https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1012443" rel="nofollow" target="_blank">Abousamra, Figgins, and Bedford (2024)</a> for background. There are four vignettes, including <a href="https://cran.r-project.org/web/packages/lineagefreq/vignettes/introduction.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/lineagefreq/vignettes/real-data-analysis.html" rel="nofollow" target="_blank">Analyzing real CDC surveillance data</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/lineagefreq.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-8" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/lineagefreq.png?w=578&#038;ssl=1" class="img-fluid" alt="Trajectories of several lineages" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=seroreconstruct" rel="nofollow" target="_blank">seroreconstruct</a> v1.1.5: Implements a Bayesian framework for inferring influenza infection status from serial antibody measurements. Jointly estimates season-specific infection probabilities, antibody boosting and waning after infection, and baseline hemagglutination inhibition titer distributions. Supports multi-season analysis and subgroup comparisons via a group_by interface. See <a href="https://www.nature.com/articles/s41467-022-29310-8" rel="nofollow" target="_blank">Tsang et al. (2022)</a> for methodological details and the two vignettes <a href="https://cran.r-project.org/web/packages/seroreconstruct/vignettes/introduction.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/seroreconstruct/vignettes/methodology.html" rel="nofollow" target="_blank">Statistical Methodology</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/sereoconstruct.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-9" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/sereoconstruct.png?w=578&#038;ssl=1" class="img-fluid" alt="Plots of antibody rise and waning" data-recalc-dims="1"></a></p>
</section>
<section id="finance" class="level3">
<h3 class="anchored" data-anchor-id="finance">Finance</h3>
<p><a href="https://cran.r-project.org/package=finlabR" rel="nofollow" target="_blank">finlabR</a> v1.0.0: Provides tools for portfolio construction and risk analytics, including mean-variance optimization, conditional value at risk minimization, risk parity, regime clustering, correlation analysis, Monte Carlo simulation, and option pricing. Includes utilities for portfolio evaluation, clustering, and risk reporting. Methods are based in part on <a href="https://onlinelibrary.wiley.com/doi/10.1111/j.1540-6261.1952.tb01525.x" rel="nofollow" target="_blank">Markowitz (1952)</a>, <a href="https://www.risk.net/journal-risk/2161159/optimization-conditional-value-risk" rel="nofollow" target="_blank">Rockafellar and Uryasev (2000)</a>, <a href="https://www.pm-research.com/content/iijpormgmt/36/4/60" rel="nofollow" target="_blank">Maillard et al. (2010)</a>, <a href="https://www.journals.uchicago.edu/doi/10.1086/260062" rel="nofollow" target="_blank">Black and Scholes (1973)</a>, and <a href="https://www.sciencedirect.com/science/article/abs/pii/0304405X79900151" rel="nofollow" target="_blank">Cox et al. (1979)</a>. See the vignettes <a href="https://cran.r-project.org/web/packages/finlabR/vignettes/finlabR-intro.html" rel="nofollow" target="_blank">Portfolio Analytics and Simulation</a> and <a href="https://cran.r-project.org/web/packages/finlabR/vignettes/finlabR-workflow.html" rel="nofollow" target="_blank">End-to-End Workflow</a></p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/finlabR.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-10" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/finlabR.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of Efficient Frontier" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=talib" rel="nofollow" target="_blank">talib</a> v0.9-2: Implements an interface to the <code>TA-Lib</code> (Technical Analysis Library) <code>C</code> library, providing access to 150+ indicators (e.g. Average Directional Movement Index (ADX), Moving Average Convergence Divergence (MACD), Relative Strength Index (RSI), Stochastic Oscillator, Bollinger Bands), candlestick pattern recognition, and rolling-window utilities. Core computations are implemented in <code>C</code> for fast Open-High-Low-Close-Volume time-series feature engineering and rule-based signal generation. There are three vignettes, including <a href="https://cran.r-project.org/web/packages/talib/vignettes/candlestick.html" rel="nofollow" target="_blank">Candlestick Pattern Recognition</a> and <a href="https://cran.r-project.org/web/packages/talib/vignettes/charting.html" rel="nofollow" target="_blank">Financial Charts</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/talib.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-11" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/talib.png?w=578&#038;ssl=1" class="img-fluid" alt="Stock chart with indicators" data-recalc-dims="1"></a></p>
</section>
<section id="functional-data-analysis" class="level3">
<h3 class="anchored" data-anchor-id="functional-data-analysis">Functional Data Analysis</h3>
<p><a href="https://cran.r-project.org/package=refundBayes" rel="nofollow" target="_blank">refundBayes</a> v0.6.0: Provides tools to perform Bayesian regression with functional data, including regression with scalar, survival, or functional outcomes. The package allows regression with scalar and functional predictors. Methods are described in <a href="https://onlinelibrary.wiley.com/doi/10.1002/sim.70265" rel="nofollow" target="_blank">Jiang et al. (2025)</a> <em>Tutorial on Bayesian Functional Regression Using Stan</em>. There are six vignettes, including <a href="https://cran.r-project.org/web/packages/refundBayes/vignettes/fofr_bayes_vignette.html" rel="nofollow" target="_blank">Bayesian Function-on-Function Regression</a> and <a href="https://cran.r-project.org/web/packages/refundBayes/vignettes/fpca_bayes_vignette.html" rel="nofollow" target="_blank">Bayesian Functional Principal Component Analysis (FPCA)</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/refundBayes.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-12" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/refundBayes.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of predictive accuracy on held-out validation set" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=tidyfun" rel="nofollow" target="_blank">tidyfun</a> v0.1.2: Builds on the <code>tf</code> package to provide functions to represent, visualize, describe and wrangle functional data in tidy data frames as well as data types for functional observations that work as columns in data frames, enabling manipulation with <code>dplyr</code> verbs and visualization with <code>ggplot2</code> geoms designed for functional data. There are six vignettes, including <a href="https://cran.r-project.org/web/packages/tidyfun/vignettes/x01_tf_Vectors.html" rel="nofollow" target="_blank">tf Vectors and Operations</a> and <a href="https://cran.r-project.org/web/packages/tidyfun/vignettes/x04_Visualization.html" rel="nofollow" target="_blank">Visualization</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/tidyfun.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-13" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/tidyfun.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of functional variables" data-recalc-dims="1"></a></p>
</section>
<section id="genomics" class="level3">
<h3 class="anchored" data-anchor-id="genomics">Genomics</h3>
<p><a href="https://cran.r-project.org/package=AbSolution" rel="nofollow" target="_blank">AbSolution</a> v1.0.1: Implements an interactive framework as a <code>Shiny</code> Application for the exploration and analysis of adaptive immune receptor repertoire sequencing data in a manner that facilitates reproducible research. It enables large-scale computation and integrated analysis of sequence-derived features, including physicochemical properties, amino acid descriptor sets, sequence motifs, compositional patterns, and somatic hypermutation metrics. See the <a href="https://github.com/EDS-Bioinformatics-Laboratory/AbSolution" rel="nofollow" target="_blank">GitHub Repository</a> and the <a href="https://cran.r-project.org/web/packages/AbSolution/vignettes/AbSolution.html" rel="nofollow" target="_blank">vignette</a> for more details.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/ABsolution.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-14" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/ABsolution.png?w=578&#038;ssl=1" class="img-fluid" alt="Snapshot of Shiny page for Exploratory Analysis" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=IOBR" rel="nofollow" target="_blank">IOBR</a> v2.2.2: Provides six modules for tumor microenvironment (TME) analysis based on multi-omics data. These modules cover data preprocessing, TME estimation, TME infiltrating patterns, cellular interactions, genome and TME interaction, and visualization for TME-relevant features, as well as modelling based on key features. In addition to providing a way to construct gene signatures from single-cell RNA-seq data, it also provides a way to construct a reference matrix for TME deconvolution from single-cell RNA-seq data. See <a href="https://www.cell.com/cell-reports-methods/fulltext/S2667-2375(24)00300-X?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS266723752400300X%3Fshowall%3Dtrue" rel="nofollow" target="_blank">Zeng et al. (2024)</a> and <a href="https://onlinelibrary.wiley.com/doi/10.1002/mdr2.70001" rel="nofollow" target="_blank">Fang et al. (2025)</a> for background and the <a href="https://cran.r-project.org/web/packages/IOBR/vignettes/IOBR-user-manual.html" rel="nofollow" target="_blank">vignette</a> for a detailed tutorial.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/IOBR.jpeg?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-15" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/IOBR.jpeg?w=578&#038;ssl=1" class="img-fluid" alt="Diagram of Workflow" data-recalc-dims="1"></a></p>
</section>
<section id="health-technology-assessment" class="level3">
<h3 class="anchored" data-anchor-id="health-technology-assessment">Health Technology Assessment</h3>
<p><a href="https://cran.r-project.org/package=htaBIM" rel="nofollow" target="_blank">htaBIM</a> v0.1.0: Implements a structured, reproducible framework, a Shiny Application, for budget impact modelling in health technology assessment (HTA), following the ISPOR Task Force guidelines <a href="https://www.valueinhealthjournal.com/article/S1098-3015(13)04235-6/fulltext?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS1098301513042356%3Fshowall%3Dtrue" rel="nofollow" target="_blank">(Sullivan et al. (2014)</a> and <a href="https://www.valueinhealthjournal.com/article/S1098-3015(10)60471-8/pdf?_returnURL=https%3A%2F%2Flinkinghub.elsevier.com%2Fretrieve%2Fpii%2FS1098301510604718%3Fshowall%3Dtrue" rel="nofollow" target="_blank">Mauskopf et al. (2007)</a> that provides functions for epidemiology-driven population estimation, market share modelling with flexible uptake dynamics, per-patient cost calculation across multiple cost categories, multi-year budget projections, payer perspective analysis, deterministic sensitivity analysis, and probabilistic sensitivity analysis. Produces submission-quality outputs including ISPOR-aligned summary tables, scenario comparison tables, per-patient cost breakdowns, tornado diagrams, PSA histograms, and text and HTML reports compatible with NICE, CADTH, and EU-HTA dossier formats. See the vignettes <a href="https://cran.r-project.org/web/packages/htaBIM/vignettes/htaBIM-introduction.html" rel="nofollow" target="_blank">Introduction</a> and <a href="https://cran.r-project.org/web/packages/htaBIM/vignettes/shiny-app.html" rel="nofollow" target="_blank">Interactive Shiny Dashboard</a>.</p>
<p><a href="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/htaBMI.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-16" rel="nofollow" target="_blank"><img src="https://i2.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/htaBMI.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of Probabilistic Sensitivity Analysis" data-recalc-dims="1"></a></p>
</section>
</div><div class="column" style="width:10%;">

</div><div class="column" style="width:45%;">
<section id="machine-learning" class="level3">
<h3 class="anchored" data-anchor-id="machine-learning">Machine Learning</h3>
<p><a href="https://cran.r-project.org/package=AntsNet" rel="nofollow" target="_blank">AntsNet</a> v1.0.0: Implements the full suite of simulation, visualization, and analysis tools for exploring the mathematical isomorphisms between ant colony decision-making and three major paradigms of machine learning: random forests (Part I: variance reduction through decorrelation), boosting (Part II: bias reduction through adaptive recruitment), and neural networks (Part III: gradient-based generational learning). See Fokoué, Babbitt, and Levental (2026) <a href="https://arxiv.org/abs/2603.20328" rel="nofollow" target="_blank">Part I</a> and <a href="https://arxiv.org/abs/2604.00038" rel="nofollow" target="_blank">Part II</a> for background and <a href="https://cran.r-project.org/web/packages/AntsNet/readme/README.html" rel="nofollow" target="_blank">README</a> to get started.</p>
<p><a href="https://cran.r-project.org/package=bigKNN" rel="nofollow" target="_blank">bigKNN</a> v0.3.0: Implements exact nearest-neighbour and radius-search routines that operate directly on <code>bigmemory::big.matrix</code> objects. Functions stream row blocks through <code>BLAS</code> kernels, support self-search and external-query search, expose prepared references for repeated queries, and can build exact k-nearest-neighbour, radius, mutual k-nearest-neighbour, and shared-nearest-neighbour graphs. There are seven vignettes, including <a href="https://cran.r-project.org/web/packages/bigKNN/vignettes/bigknn-quickstart.html" rel="nofollow" target="_blank">Quick Start</a> and <a href="https://cran.r-project.org/web/packages/bigKNN/vignettes/bigknn-evaluating-approximate-search.html" rel="nofollow" target="_blank">Using bigKNN as Exact Ground Truth</a>.</p>
</section>
<section id="medical-statistics" class="level3">
<h3 class="anchored" data-anchor-id="medical-statistics">Medical Statistics</h3>
<p><a href="https://gosukehommaex.github.io/BayesianQDM/" rel="nofollow" target="_blank">BayesianQDM</a> v0.1.0: Provides comprehensive methods to calculate posterior probabilities, posterior predictive probabilities, and Go/NoGo/Gray decision probabilities for quantitative decision-making under a Bayesian paradigm in clinical trials. Supports both single and two-endpoint analyses for binary and continuous outcomes, with controlled, uncontrolled, and external designs. External designs incorporate historical data through power priors using exact conjugate representations to significantly reduce computational burden while preserving complete Bayesian rigor. See <a href="https://www.tandfonline.com/doi/full/10.1080/10543406.2026.2655410" rel="nofollow" target="_blank">Kang, Yamaguchi, and Han (2026)</a> for the methodological framework. There are five vignettes, including <a href="https://cran.r-project.org/web/packages/BayesianQDM/vignettes/overview.html" rel="nofollow" target="_blank">Overview</a> and <a href="https://cran.r-project.org/web/packages/BayesianQDM/vignettes/two-continuous.html" rel="nofollow" target="_blank">Two Continuous Endpoints</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/BayesianQDM.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-17" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/BayesianQDM.png?w=578&#038;ssl=1" class="img-fluid" alt="An Optimal Threshold Plot" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=raretrans" rel="nofollow" target="_blank">raretrans</a> v1.0.5: Provides functions to correct biased transition and fertility estimates in population projection matrices caused by small sample sizes, never observed biologically possible transmissions, or transitions estimated at 100% survival, stasis, or mortality that are biologically implausible. Implements a multinomial-Dirichlet Bayesian prior for transition probabilities and a Gamma-Poisson prior for reproduction, allowing analysts to incorporate prior biological knowledge and regularise estimates from rare or unobserved events. Methods are described in <a href="https://www.sciencedirect.com/science/article/abs/pii/S0304380021000971" rel="nofollow" target="_blank">Tremblay et al. (2021)</a>. There are six vignettes, including both a <a href="https://cran.r-project.org/web/packages/raretrans/vignettes/quick_start.html" rel="nofollow" target="_blank">Quick start</a> and an <a href="https://cran.r-project.org/web/packages/raretrans/vignettes/introduction.html" rel="nofollow" target="_blank">Introduction</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/raretrans.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-18" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/raretrans.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot showing effect of prior weight oncredible intervals" data-recalc-dims="1"></a></p>
</section>
<section id="meta-analysis" class="level3">
<h3 class="anchored" data-anchor-id="meta-analysis">Meta Analysis</h3>
<p><a href="https://cran.r-project.org/package=confMeta" rel="nofollow" target="_blank">confMeta</a> v0.1.0: Provides tools for the combination of individual study results in meta-analyses using <em>p-value</em> functions. Implements various combination methods, including those by Fisher, Stouffer, Tippett, Edgington, along with weighted generalizations. Contains functionality for the visualization and calculation of confidence curves and drapery plots to summarize evidence across studies. See the <a href="https://cran.r-project.org/web/packages/confMeta/vignettes/confMeta-usage.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/confMeta.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-19" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/confMeta.png?w=578&#038;ssl=1" class="img-fluid" alt="Combined p=value and effects plots" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=drmeta" rel="nofollow" target="_blank">drmeta</a> v0.1.0: Implements a variance-function random-effects framework in which between-study heterogeneity is modeled as a function of a study-level design robustness index, allowing heterogeneity to depend systematically on study quality or design strength rather than being treated as a single nuisance parameter. The framework nests classical fixed-effects and standard random-effects meta-analysis as special cases, making it a strict generalization of existing approaches. See the <a href="https://cran.r-project.org/web/packages/drmeta/vignettes/getting-started.html" rel="nofollow" target="_blank">Getting Started Guide</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/drmeta.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-20" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/drmeta.png?w=578&#038;ssl=1" class="img-fluid" alt="Funnel plot showing Standard error vs Effect Size" data-recalc-dims="1"></a></p>
</section>
<section id="networks" class="level3">
<h3 class="anchored" data-anchor-id="networks">Networks</h3>
<p><a href="https://cran.r-project.org/package=sparsecommunity" rel="nofollow" target="_blank">sparsecommunity</a> v0.1.1: Implements spectral clustering algorithms for community detection in sparse networks under the stochastic block model and degree-corrected stochastic block model following the methods of <a href="https://projecteuclid.org/journals/annals-of-statistics/volume-43/issue-1/Consistency-of-spectral-clustering-in-stochastic-block-models/10.1214/14-AOS1274.full" rel="nofollow" target="_blank">Lei and Rinaldo (2015)</a>. Provides a regularized normalized Laplacian embedding, spherical k-median clustering, simulation utilities, and a misclustering rate evaluation metric along with the NCAA college football network of <a href="https://www.pnas.org/doi/full/10.1073/pnas.122653799" rel="nofollow" target="_blank">Girvan and Newman (2002)</a> as a benchmark dataset, and the Bethe-Hessian community number estimator of <a href="https://www.tandfonline.com/doi/full/10.1080/01621459.2023.2223793" rel="nofollow" target="_blank">Hwang (2023)</a>. See the <a href="https://cran.r-project.org/web/packages/sparsecommunity/vignettes/sparsecommunity-intro.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/sparsecommunity.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-21" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/sparsecommunity.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of detetcted vs. true communities" data-recalc-dims="1"></a></p>
</section>
<section id="physics" class="level3">
<h3 class="anchored" data-anchor-id="physics">Physics</h3>
<p><a href="https://cran.r-project.org/package=orbitr" rel="nofollow" target="_blank">orbitr</a> v0.3.0: Provides a lightweight, fully vectorized N-body physics engine built for the R ecosystem. Simulate and visualize complex orbital mechanics, celestial trajectories, and gravitational interactions using tidy data principles. Features multiple numerical integration methods, including the energy-conserving velocity Verlet algorithm <a href="https://journals.aps.org/pr/abstract/10.1103/PhysRev.159.98" rel="nofollow" target="_blank">Verlet (1967)</a> to ensure highly stable orbital propagation. Gravitational N-body methods follow <a href="https://www.cambridge.org/us/universitypress/subjects/physics/astrophysics/gravitational-n-body-simulations-tools-and-algorithms?format=PB&#038;isbn=9780521121538" rel="nofollow" target="_blank">Aarseth (2003)</a>. There are twelve vignettes, including <a href="https://cran.r-project.org/web/packages/orbitr/vignettes/keplerian-elements.html" rel="nofollow" target="_blank">Quick Start Guide</a> and <a href="https://cran.r-project.org/web/packages/orbitr/vignettes/the-physics.html" rel="nofollow" target="_blank">The Physics</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/orbitr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-22" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/orbitr.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of planetary orbits" data-recalc-dims="1"></a></p>
</section>
<section id="programming" class="level3">
<h3 class="anchored" data-anchor-id="programming">Programming</h3>
<p><a href="https://cran.r-project.org/package=mori" rel="nofollow" target="_blank">mori</a> v0.2.0: Enables users to share <code>R</code> objects across processes on the same machine via a single copy in <code>POSIX</code> shared memory (<code>Linux</code>, <code>macOS</code>) or a <code>Win32</code> file mapping (<code>Windows</code>). Every process reads from the same physical pages through the <code>R</code> Alternative Representation (<code>ALTREP</code>) framework, giving lazy, zero-copy access. Shared objects serialize compactly as their shared memory name rather than their full contents. See <a href="https://cran.r-project.org/web/packages/mori/readme/README.html" rel="nofollow" target="_blank">README</a> to get started.</p>
<p><a href="https://cran.r-project.org/package=progressify" rel="nofollow" target="_blank">progressify</a> v0.1.0: The `<code>progressify()</code> function rewrites (transpiles) calls to sequential and parallel map-reduce functions such as <code>base::lapply()</code>, <code>purrr::map()</code>, <code>foreach::foreach()</code>, and <code>plyr::llply()</code> to signal progress updates. By combining this function with R’s native pipe operator, you have a straightforward way to report progress on iterative computations with minimal refactoring, e.g., <code>lapply(x, fcn) |&gt; progressify()</code> and <code>purrr::map(x, fcn) |&gt; progressify()</code>. It is compatible with the <code>futurize</code> package for parallelization, e.g. <code>lapply(x, fcn) |&gt; progressify() |&gt; futurize()</code> and <code>purrr::map(x, fcn) |&gt; futurize() |&gt; progressify()</code>. There are nine brief vignettes, including <a href="https://cran.r-project.org/web/packages/progressify/vignettes/progressify-11-base.html" rel="nofollow" target="_blank">Progress updates for base-R apply functions</a> and <a href="https://cran.r-project.org/web/packages/progressify/vignettes/progressify-21-purrr.html" rel="nofollow" target="_blank">Progress updates for ‘purrr’ functions</a>.</p>
<p><a href="https://cran.r-project.org/package=shard" rel="nofollow" target="_blank">shard</a> v0.1.1: Provides a parallel execution runtime for R that emphasizes deterministic memory behavior and efficient handling of large shared inputs, which enables zero-copy parallel reads via shared, memory-mapped segments, encourages explicit output buffers to avoid large result aggregation, and supervises worker processes to mitigate memory drift via controlled recycling. Diagnostics report peak memory usage, end-of-run memory return, and hidden copy/materialization events to support reproducible performance benchmarking. See the <a href="https://cran.r-project.org/web/packages/shard/vignettes/shard.html" rel="nofollow" target="_blank">vignette</a> to get started.</p>
</section>
<section id="statistics" class="level3">
<h3 class="anchored" data-anchor-id="statistics">Statistics</h3>
<p><a href="https://cran.r-project.org/package=balnet" rel="nofollow" target="_blank">balnet</a> v0.0.3: Provides pathwise estimation of regularized logistic propensity score models using covariate balancing loss functions rather than maximum likelihood. Regularization paths are fit via the <code>adelie</code> elastic-net solver with a <code>glmnet</code>-like interface, yielding balancing weights that target covariate balance for the ATE and ATT. Under lasso penalization, lambda bounds the maximum covariate imbalance, so the regularization path traces a sequence of decreasing imbalance tolerances. See <a href="https://arxiv.org/abs/2602.18577" rel="nofollow" target="_blank">Sverdrup &#038; Hastie (2026)</a> for details and the <a href="https://cran.r-project.org/web/packages/balnet/vignettes/balnet.html" rel="nofollow" target="_blank">vignette</a> for an introduction.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/balnet.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-23" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/balnet.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot showing model fit that aims to balance control covariate means toward those of the treated group" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=ngme2" rel="nofollow" target="_blank">ngme2</a> v0.9.8: Functions to fit and analyze linear latent non-Gaussian models for temporal, spatial, and space-time data, including autoregressive and Ornstein-Uhlenbeck processes, random walks, Matern fields based on stochastic partial differential equations, separable and non-separable space-time models, graph-based Matern models, bivariate type-G fields, and user-defined sparse operators. Latent fields and observation models can use Gaussian and non-Gaussian noise distributions. The modeling framework is described in <a href="https://arxiv.org/abs/2602.23987" rel="nofollow" target="_blank">Bolin et al. (2026)</a>. See the <a href="https://davidbolin.github.io/ngme2/articles/tensor-product.html" rel="nofollow" target="_blank">vignette</a> for exceptionally well-done documentation.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/ngme2.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-24" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/ngme2.png?w=578&#038;ssl=1" class="img-fluid" alt="Simulated Space-Time Field" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=nowcastr" rel="nofollow" target="_blank">nowcastr</a> v0.2.0: Implements tools for performing nowcasting using the <a href="https://en.wikipedia.org/wiki/Chain-ladder_method" rel="nofollow" target="_blank">Chain-Ladder method</a>, and supports both non-cumulative delay-based estimation and model-based completeness fitting (e.g., using logistic or Gompertz curves) to predict final counts from partially reported data. See the vignettes <a href="https://cran.r-project.org/web/packages/nowcastr/vignettes/nowcastr.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/nowcastr/vignettes/eval.html" rel="nofollow" target="_blank">Evaluate Past Nowcasts Accuracy</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/nowcastr.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-25" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/nowcastr.png?w=578&#038;ssl=1" class="img-fluid" alt="Nowcast Evaluation Plots" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=TwoStepSDFM" rel="nofollow" target="_blank">TwoStepSDFM</a> v0.2.2: Provides functions to estimate a sparse Gaussian state-space model with mixed frequency data via sparse principal components analysis and the Kalman filter and smoother. For more details, see <a href="https://papers.ssrn.com/sol3/papers.cfm?abstract_id=4733872" rel="nofollow" target="_blank">Franjic and Schweikert (2024)</a>. The <a href="https://cran.r-project.org/web/packages/TwoStepSDFM/vignettes/IntroTwoStepSDFM.html" rel="nofollow" target="_blank">vignette</a> provides an introduction.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/TwoStepSDFM.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-26" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/TwoStepSDFM.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of Quarterly and Monthly Data" data-recalc-dims="1"></a></p>
</section>
<section id="time-series" class="level3">
<h3 class="anchored" data-anchor-id="time-series">Time Series</h3>
<p><a href="https://cran.r-project.org/package=MatchingPursuit" rel="nofollow" target="_blank">MatchingPursuit</a> v1.0.1: Provides tools for analyzing and decomposing time series data using the Matching Pursuit algorithm, a greedy signal decomposition technique that represents complex signals as a linear combination of simpler functions (called atoms) selected from a redundant dictionary. For more details, see <a href="https://ieeexplore.ieee.org/document/258082" rel="nofollow" target="_blank">Mallat and Zhang (1993)</a>, <a href="https://ieeexplore.ieee.org/document/342465" rel="nofollow" target="_blank">Pati et al. (1993)</a>, <a href="https://link.springer.com/book/10.1007/978-1-4419-7011-4" rel="nofollow" target="_blank">Elad (2010)</a>, and <a href="https://dl.acm.org/doi/10.1145/3674832" rel="nofollow" target="_blank">Różański (2024)</a>. See the <a href="https://cran.r-project.org/web/packages/MatchingPursuit/vignettes/MatchingPursuit.html" rel="nofollow" target="_blank">vignette</a>.</p>
<p><a href="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/MatchingPursuit.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-27" rel="nofollow" target="_blank"><img src="https://i0.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/MatchingPursuit.png?w=578&#038;ssl=1" class="img-fluid" alt="Frequency plot of a time series" data-recalc-dims="1"></a></p>
<p><a href="https://cran.r-project.org/package=xiacf" rel="nofollow" target="_blank">xiacf</a> v0.5.0: Computes Chatterjee’s non-parametric correlation coefficient for time series data. It extends the original metric to time series analysis by providing the Xi-Autocorrelation Function and Xi-Cross-Correlation Function. Allows users to test for non-linear dependence using Iterative Amplitude Adjusted Fourier Transform surrogate data with strict Family-Wise Error Rate control via Max-statistic approaches. Methodologies are based on <a href="https://www.tandfonline.com/doi/full/10.1080/01621459.2020.1758115" rel="nofollow" target="_blank">Chatterjee (2021)</a> and surrogate data testing methods by <a href="https://journals.aps.org/prl/abstract/10.1103/PhysRevLett.77.635" rel="nofollow" target="_blank">Schreiber and Schmitz (1996)</a>. See <a href="https://cran.r-project.org/web/packages/xiacf/readme/README.html" rel="nofollow" target="_blank">README</a> for examples.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/xiacf.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-28" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/xiacf.png?w=578&#038;ssl=1" class="img-fluid" alt="Plot of mltivariate Xi correlation matrix" data-recalc-dims="1"></a></p>
</section>
<section id="utilities" class="level3">
<h3 class="anchored" data-anchor-id="utilities">Utilities</h3>
<p><a href="https://cran.r-project.org/package=abba" rel="nofollow" target="_blank">abba</a> v0.2.0: Enables users to submit and monitor batch execution of R programs across distributed computing backends, including <code>Kubernetes</code>, <code>SLURM</code>, and <code>Posit Workbench</code>. Provides end-user job submission functions, cluster interface functions using <code>kubectl</code> and <code>SLURM</code> commands, and a <code>plumber</code> API template for secure identity segregation. Supports parallel and sequential batch execution, file-based caching to skip unchanged programs, and <code>logrx</code> integration for execution logging. There are seven vignettes, including <a href="https://cran.r-project.org/web/packages/abba/vignettes/k8s_batch_job.html" rel="nofollow" target="_blank">Kubernetes Batch Jobs</a> and <a href="https://cran.r-project.org/web/packages/abba/vignettes/slurm_job_submission.html" rel="nofollow" target="_blank">SLURM Job Submission</a>.</p>
<p><a href="https://cran.r-project.org/package=vectra" rel="nofollow" target="_blank">vectra</a> v0.6.2: Implements a minimal columnar query engine with lazy execution on datasets larger than RAM. Provides <code>dplyr</code>-like verbs (<code>filter()</code>, <code>select()</code>, <code>mutate()</code>, <code>group_by()</code>, <code>summarise()</code>, joins, window functions) and common aggregations (<code>n()</code>, <code>sum()</code>, <code>mean()</code>, <code>min()</code>, <code>max()</code>, <code>sd(</code>), <code>first()</code>,<code>last()</code>) backed by a pure <code>C11</code> pull-based execution engine and a custom on-disk format (<code>.vtr</code>). Reads and writes <code>GeoTIFF</code> (including tiled and <code>BigTIFF</code> layouts) and a tiled raster format (<code>.vec</code>) with overview pyramids and time cubes for larger-than-RAM raster data. There are eight vignettes, including <a href="https://cran.r-project.org/web/packages/vectra/vignettes/quickstart.html" rel="nofollow" target="_blank">Getting Started</a> and <a href="https://cran.r-project.org/web/packages/vectra/vignettes/string-ops.html" rel="nofollow" target="_blank">String Operations and Fuzzy Matching</a>.</p>
</section>
<section id="visualization" class="level3">
<h3 class="anchored" data-anchor-id="visualization">Visualization</h3>
<p><a href="https://cran.r-project.org/package=janusplot" rel="nofollow" target="_blank">janusplot</a> v0.1.0: Provides functions to render a pairwise, asymmetric smoothed-association matrix of continuous variables. Each cell shows the fitted spline from an <code>mgcv</code> generalized additive model, with the upper triangle displaying <code>gam(x_j ~ s(x_i))</code> and the lower triangle <code>gam(x_i ~ s(x_j))</code>. Unlike Pearson’s correlation matrix, the visualization is intentionally asymmetric, revealing heteroscedasticity, leverage, and directional non-linearity that a single scalar correlation hides. An asymmetry index and a 24-category shape taxonomy quantify the directional difference and qualitative form of each fitted smooth. There are two vignettes, <a href="https://cran.r-project.org/web/packages/janusplot/vignettes/janusplot.html" rel="nofollow" target="_blank">Asymmetric Smoothed-Association Matrices</a> and <a href="https://cran.r-project.org/web/packages/janusplot/vignettes/shape-recognition-sensitivity.html" rel="nofollow" target="_blank">Shape-recognition sensitivity study</a>.</p>
<p><a href="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/janusplot.png?ssl=1" class="lightbox" data-gallery="quarto-lightbox-gallery-29" rel="nofollow" target="_blank"><img src="https://i1.wp.com/rworks.dev/posts/april-2026-top-40-new-cran-packages/janusplot.png?w=578&#038;ssl=1" class="img-fluid" alt="Example of a janusplot" data-recalc-dims="1"></a></p>
</section>
</div>
</div>



 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rworks.dev/posts/april-2026-top-40-new-cran-packages/"> R Works</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/april-2026-top-40-new-cran-packages/">April 2026 Top 40 New CRAN Packages</a>]]></content:encoded>
					
		
		<enclosure url="https://rworks.dev/posts/april-2026-top-40-new-cran-packages/MatchingPursuit.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401552</post-id>	</item>
		<item>
		<title>[R] How to Position the Legend Inside a Plot in ggplot2</title>
		<link>https://www.r-bloggers.com/2026/05/r-how-to-position-the-legend-inside-a-plot-in-ggplot2/</link>
		
		<dc:creator><![CDATA[R on Zhenguo Zhang&#039;s Blog]]></dc:creator>
		<pubDate>Sat, 30 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Zhenguo Zhang's Blog https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/ -</p>
<p>By default, ggplot2 places the legend outside the plot area (usually on the right). However, sometimes you may want to move the legend inside the plot to sa...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/r-how-to-position-the-legend-inside-a-plot-in-ggplot2/">[R] How to Position the Legend Inside a Plot in ggplot2</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/"> R on Zhenguo Zhang&#039;s Blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
Zhenguo Zhang&#8217;s Blog https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/ &#8211;


<p>By default, <code>ggplot2</code> places the legend outside the plot area (usually on the right). However, sometimes you may want to move the legend inside the plot to save space or improve the layout. This post explores how to achieve this using <code>theme()</code> parameters.</p>
<div id="example-1-basic-usage-of-legend.position" class="section level2">
<h2>Example 1: Basic Usage of <code>legend.position</code></h2>
<p>The simplest way to move a legend inside the plot is by providing a numeric vector of length two to the <code>legend.position</code> argument in <code>theme()</code>. These coordinates represent the relative position within the plot panel, ranging from 0 to 1. Check the
section on <a href="https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/#key-positioning-controls" rel="nofollow" target="_blank">“Key Positioning Controls”</a> at the end for a detailed explanation of the coordinate system.</p>
<pre>library(ggplot2)

ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  theme_bw() +
  theme(legend.position = c(0.8, 0.8))</pre>
<p><img src="https://i2.wp.com/fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/index_files/figure-html/example1-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="example-2-fine-tuning-with-justification-and-background" class="section level2">
<h2>Example 2: Fine-Tuning with Justification and Background</h2>
<p>When you place a legend inside, you often need to adjust which part of the legend box aligns with your coordinates and handle the background if it obscures data.</p>
<ul>
<li><strong><code>legend.justification</code></strong>: Controls the anchor point of the legend box. For example, <code>c(&quot;right&quot;, &quot;top&quot;)</code> means the top-right corner of the legend box will be placed at the specified coordinates.</li>
<li><strong><code>legend.background</code></strong>: Use <code>element_blank()</code> to make the background transparent.</li>
</ul>
<pre>ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  theme_bw() +
  theme(
    legend.position = c(1, 1),
    legend.justification = c(&quot;right&quot;, &quot;top&quot;),
    legend.background = element_blank()
  )</pre>
<p><img src="https://i1.wp.com/fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/index_files/figure-html/example2-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="example-3-positioning-in-faceted-plots" class="section level2">
<h2>Example 3: Positioning in Faceted Plots</h2>
<p>Moving the legend inside a faceted plot works similarly: the whole figure is considered as
one plot for the coordinate system. You can use the coordinates to place it within one of the empty spaces or over a specific panel.
In the following example, the legend is placed in the bottom center of the plot, and a background is added to improve readability:</p>
<pre>ggplot(mtcars, aes(wt, mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  facet_wrap(~am) +
  theme_bw() +
  theme(
    legend.position = c(0.5, 0.2),
    legend.background = element_rect(fill = &quot;white&quot;, color = &quot;grey80&quot;)
  )</pre>
<p><img src="https://i1.wp.com/fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/index_files/figure-html/example3-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="example-4-the-empty-space-trick-for-faceted-plots" class="section level2">
<h2>Example 4: The “Empty Space” Trick for Faceted Plots</h2>
<p>If you have a panel which doesn’t have any data, and then it is the perfect spot to
place legend. To show this, the facet variable <code>carb</code> has only 3 values, and we
will make a 2&#215;2 facet grid. We will also remove the background and border of the legend.
The following example shows this:</p>
<pre># Use 3 categories to leave the 4th spot in a 2x2 grid empty
mtcars_subset &lt;- mtcars[mtcars$carb %in% c(1, 2, 4), ]

ggplot(mtcars_subset, aes(wt, mpg, color = factor(cyl))) +
  geom_point(size = 3) +
  facet_wrap(~carb, nrow = 2, ncol = 2) +
  theme_bw() +
  theme(
    # Coordinates c(0.75, 0.25) target the center of the empty bottom-right area
    legend.position = c(0.75, 0.25),
    legend.background = element_blank(),
    legend.justification = &quot;center&quot;
  )</pre>
<p><img src="https://i2.wp.com/fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/index_files/figure-html/example4-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="key-positioning-controls" class="section level2">
<h2>Key Positioning Controls</h2>
<p>To master legend positioning, keep these three controls in mind:</p>
<ol style="list-style-type: decimal">
<li><strong>Coordinate System</strong>: The coordinates <code>c(X, Y)</code> represent a relative scale from 0 to 1 across the plot panel.
<ul>
<li><code>c(0, 0)</code>: Bottom-left corner</li>
<li><code>c(1, 1)</code>: Top-right corner</li>
<li><code>c(0.5, 0.5)</code>: Center of the plot</li>
</ul></li>
<li><strong>Anchor Point (<code>legend.justification</code>)</strong>: Use this to control which part of the legend box aligns with your coordinates. For example, <code>c(&quot;right&quot;, &quot;top&quot;)</code> ensures the top-right corner of the legend box sits exactly at your target coordinate.</li>
<li><strong>Background Removal</strong>: If your legend blocks data points, clear the legend box background using <code>legend.background = element_blank()</code>. Alternatively, use <code>element_rect()</code> to give it a semi-transparent or styled background.</li>
</ol>
<p>Happy Programming!</p>
</div>
- https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/ - 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://fortune9.netlify.app/2026/05/30/r-position-legend-inside-plot/"> R on Zhenguo Zhang&#039;s Blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/r-how-to-position-the-legend-inside-a-plot-in-ggplot2/">[R] How to Position the Legend Inside a Plot in ggplot2</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401550</post-id>	</item>
		<item>
		<title>EuroBioC2026 Tidyomics Hackathon</title>
		<link>https://www.r-bloggers.com/2026/05/eurobioc2026-tidyomics-hackathon/</link>
		
		<dc:creator><![CDATA[Stefano Mangiola]]></dc:creator>
		<pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Join us in Turku<br />
The Tidyomics community is organising a hackathon during the pre-conference programme of EuroBioC2026 in Turku, Finland. The hackathon will take place on June 1-2, 2026, ahead of the main EuroBioC2026 conference on June 3-5, 20...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/eurobioc2026-tidyomics-hackathon/">EuroBioC2026 Tidyomics Hackathon</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/"> tidyomicsBlog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<section id="join-us-in-turku" class="level1">
<h1>Join us in Turku</h1>
<p>The Tidyomics community is organising a hackathon during the pre-conference programme of <a href="https://eurobioc2026.bioconductor.org/" rel="nofollow" target="_blank">EuroBioC2026</a> in Turku, Finland. The hackathon will take place on <strong>June 1-2, 2026</strong>, ahead of the main EuroBioC2026 conference on <strong>June 3-5, 2026</strong>.</p>
<p>This is a community event for people who build, use, teach, document, or are curious about tidy interfaces for omics data analysis in R. Whether you are a regular contributor or new to Tidyomics, the hackathon is a chance to meet collaborators, turn ideas into concrete issues, and make progress on packages, tutorials, and documentation together.</p>
<p>Grab your Tidyomics stickers during the conference! Thanks to Bea Campillo Minano for printing them.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i2.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/tidyomics-sticker-giveaway.png?w=578&#038;ssl=1" class="cover-image img-fluid figure-img" style="width:70.0%" data-recalc-dims="1"></p>
<figcaption>Tidyomics stickers for hackathon participants</figcaption>
</figure>
</div>
</section>
<section id="what-we-will-work-on" class="level1">
<h1>What we will work on</h1>
<p>The hackathon will focus on practical, community-led work across the Tidyomics ecosystem. Possible projects include:</p>
<ul>
<li>fixing bugs and closing issues across Tidyomics packages</li>
<li>implementing new features and workflow improvements</li>
<li>improving documentation, tutorials, and vignettes</li>
<li>developing examples that make tidy omics workflows easier to learn and reuse</li>
</ul>
<p>The <a href="https://github.com/orgs/tidyomics/projects/1" rel="nofollow" target="_blank">Tidyomics open challenges board</a> will be the main source of ideas for the hackathon. Before the event, please browse the board and identify problems or projects you would like to work on.</p>
</section>
<section id="how-to-participate" class="level1">
<h1>How to participate</h1>
<p>Please start from the <a href="https://github.com/tidyomics/tidyomicsHackathonTurku2026" rel="nofollow" target="_blank">Tidyomics Hackathon Turku 2026 GitHub repository</a>. The repository includes event details, preparation steps, and space for participants to open or discuss hackathon issues.</p>
<p>Before the event:</p>
<ol type="1">
<li>Browse the <a href="https://github.com/orgs/tidyomics/projects/1" rel="nofollow" target="_blank">Tidyomics open challenges</a>.</li>
<li>Select a problem you are interested in.</li>
<li>Open an issue in the <a href="https://github.com/tidyomics/tidyomicsHackathonTurku2026" rel="nofollow" target="_blank">hackathon repository</a>, or comment/react if an issue already exists.</li>
<li>Join the community discussion in the <a href="https://community-bioc.zulipchat.com/#narrow/channel/595963-eurobioc2026-turku-hackathon" rel="nofollow" target="_blank">EuroBioC2026 Turku hackathon Zulip channel</a>.</li>
</ol>
<p>Groups and projects will be finalised during the first day, based on open issues, participant interest, and available expertise.</p>
</section>
<section id="communication" class="level1">
<h1>Communication</h1>
<p>The official discussion space for the hackathon is the <a href="https://community-bioc.zulipchat.com/#narrow/channel/595963-eurobioc2026-turku-hackathon" rel="nofollow" target="_blank">EuroBioC2026 Turku hackathon channel on the Bioconductor Zulip server</a>. The virtual meeting room link will also be shared through that channel and by email.</p>
<p>If you are attending EuroBioC2026 online, note that the conference provides <a href="https://eurobioc2026.bioconductor.org/" rel="nofollow" target="_blank">free live streaming</a> for remote participants.</p>
<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><img src="https://i0.wp.com/tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/EuroBioC2026_sticker.png?w=578&#038;ssl=1" class="img-fluid figure-img" style="width:35.0%" data-recalc-dims="1"></p>
<figcaption>EuroBioC2026 Turku conference sticker</figcaption>
</figure>
</div>
</section>
<section id="people" class="level1">
<h1>People</h1>
<p>The hackathon was proposed by <strong>Nicholas Cooley</strong>.</p>
<p>The organising committee is:</p>
<ul>
<li>Juan Henao</li>
<li>Stevie Pederson</li>
<li>Michael Love</li>
<li>Nicholas Cooley</li>
</ul>
</section>
<section id="see-you-there" class="level1">
<h1>See you there</h1>
<p>EuroBioC2026 is an opportunity to bring the Bioconductor and Tidyomics communities together around shared tools, shared challenges, and shared documentation. We hope to see you in Turku, on Zulip, and in the hackathon repository.</p>


</section>

<p>
© 2025 tidyomics. Content is published under <a href="https://creativecommons.org/licenses/by/4.0/" rel="nofollow" target="_blank">Creative Commons CC-BY-4.0 License</a> for the text and <a href="https://opensource.org/licenses/BSD-3-Clause" rel="nofollow" target="_blank">BSD 3-Clause License</a> for any code. | <a href="https://www.r-bloggers.com/" rel="nofollow" target="_blank">R-Bloggers</a>
</p> 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/"> tidyomicsBlog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/eurobioc2026-tidyomics-hackathon/">EuroBioC2026 Tidyomics Hackathon</a>]]></content:encoded>
					
		
		<enclosure url="https://tidyomics.github.io/tidyomicsBlog/posts/2026-05-29-eurobioc2026-tidyomics-hackathon/tidyomics-sticker-giveaway.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401539</post-id>	</item>
		<item>
		<title>Behavior-Driven Development in R Shiny: Asserting Outcomes with Then Steps</title>
		<link>https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-asserting-outcomes-with-then-steps/</link>
		
		<dc:creator><![CDATA[jakub::sobolewski]]></dc:creator>
		<pubDate>Fri, 29 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://jakubsobolewski.com/blog/bdd-shiny-then</guid>

					<description><![CDATA[<p>Learn how to write Then steps that assert outcomes without coupling to implementation. Build custom testthat expectations and keep your BDD assertions at the right level.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-asserting-outcomes-with-then-steps/">Behavior-Driven Development in R Shiny: Asserting Outcomes with Then Steps</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-then"> jakub::sobolewski</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>You’ve set up all the preconditions.</p>
<p>You’ve triggered the action.</p>
<p>Now the specification needs to answer one question: did the system do the right thing?</p>
<p>That’s what Then steps are for. How precisely you answer it determines whether your specifications act as a reliable safety net or produce false confidence.</p>
<hr>
<p>This article is the 3rd part of a series on writing BDD specifications for Shiny applications. We’ve built a data submission form, managed preconditions with Given steps, and modeled user interactions with When steps.</p>
<p>Read the previous articles to get full context, or continue here to focus on writing Then steps.</p>
<ol>
<li><strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-feature/" rel="nofollow" target="_blank">Behavior-Driven Development in R Shiny: A Step-By-Step Example</a></strong></li>
<li><strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-given/" rel="nofollow" target="_blank">Behavior-Driven Development in R Shiny: Setting Up Test Preconditions with Given Steps</a></strong></li>
<li><strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-when/" rel="nofollow" target="_blank">Behavior-Driven Development in R Shiny: Writing When Steps That Model User Behavior</a></strong></li>
</ol>
<hr>
<blockquote>
<p><a href="https://jakubsobolewski.com/get-roadmap/" rel="nofollow" target="_blank">Level-up your testing game! Grab your copy of the R testing roadmap.</a></p>
</blockquote>
<h2 id="the-purpose-of-then">The Purpose of Then</h2>
<p>Then steps answer one question: <strong>What changed as a result of the user’s action?</strong></p>
<p>Not how it changed internally. What the user can now observe — or what the system has now done — that wasn’t true before.</p>
<p>This distinction shapes every Then step you write. The assertion lives at the level of the outcome, not at the level of the mechanism that produced it.</p>
<pre># Outcome — what the user cares about
then_there_are_entries &lt;- function(context, n) {
  expect_equal(context$storage$size(), n)
  context
}

# Mechanism — how the app stored it internally
then_the_database_table_has_n_rows &lt;- function(context, n) {
  expect_equal(nrow(DBI::dbReadTable(context$conn, &quot;entries&quot;)), n)
  context
}</pre>
<p>Both assertions verify the same underlying fact — that the entry was saved — but the first one survives a switch from disk cache to database to API. The second breaks the moment you change the storage technology.</p>
<p><strong>Assert what the user or system guarantees. Leave how it’s achieved to implementation tests.</strong></p>
<h2 id="what-to-assert">What to Assert</h2>
<p>Then steps verify outcomes across three kinds of observable evidence. These categories describe <em>what</em> you can check — they don’t prescribe how many steps to use. How to group assertions into steps is a separate question, covered in the next section.</p>
<h3 id="user-visible-state">User-visible state</h3>
<p>Did the UI change the way a user would expect?</p>
<pre>then_i_am_prompted_to_provide_required_fields &lt;- function(context) {
  context$driver$expect_validation_feedback()
  context
}</pre>
<p>Assertions use driver methods — the same translation layer that When steps use — so they’re protected from UI implementation details.</p>
<h3 id="system-state">System state</h3>
<p>Sometimes the most meaningful evidence isn’t what the user sees but what the system now holds.</p>
<pre>then_the_entry_has_title &lt;- function(context, expected_title) {
  entry &lt;- context$storage$get_first()
  expect_equal(entry$title, expected_title)
  context
}</pre>
<p>These assertions reach directly into the storage object that Given steps configured. They’re faster than UI assertions and more precise: they check the exact state of the system without waiting for the browser to render anything.</p>
<h3 id="side-effects">Side effects</h3>
<p>Some behaviors produce no visible UI change and no stored data — they trigger side effects: emails, API calls, log entries. These still need verification.</p>
<pre>then_email_notification_is_sent &lt;- function(context) {
  context$email_service$expect_sent()
  context
}

then_i_am_informed_email_was_not_sent &lt;- function(context) {
  context$driver$expect_visible(&quot;email_failure_message&quot;)
  context
}</pre>
<p>The first assertion works because the email service test double (set up in Given) records whether it was called. The second checks that the app communicated the failure to the user — which is the behavior the specification is actually about.</p>
<h2 id="implementing-then-steps-in-the-driver">Implementing Then Steps in the Driver</h2>
<p>Then steps that check UI state belong in the driver, just like When steps. This keeps the assertion details hidden from the specification:</p>
<pre>#' tests/testthat/setup-driver.R
MyAppDriver &lt;- R6::R6Class(
  classname = &quot;MyAppDriver&quot;,
  inherit = shinytest2::AppDriver,
  public = list(
    expect_visible = function(output_id) {
      # Find if HTML element is visible
      invisible(self)
    },
    expect_entry_count = function(n) {
      # Find number of HTML elements
      invisible(self)
    },
    expect_validation_feedback = function() {
      # Check if HTML element is visible
      invisible(self)
    }
  )
)</pre>
<p>Each driver method is a named, reusable assertion. If the HTML structure changes, you fix it in one place.</p>
<h2 id="grouping-then-steps">Grouping Then Steps</h2>
<p>The right question is not “how many assertions per step?” but “which assertions belong together?”</p>
<p><strong>Group assertions that describe the same observable behavior. Split when outcomes can diverge independently.</strong></p>
<p>Consider what happens when a user successfully submits an entry: the storage grows by one, and a confirmation message appears. These two outcomes are two sides of the same coin — if one is true, the other should be too. Splitting them into separate steps implies they can diverge, which invites the reader to wonder what it would mean for storage to succeed but no confirmation to appear, or vice versa. Grouping them into a single step names the behavior directly:</p>
<pre>then_entry_is_submitted &lt;- function(context) {
  expect_equal(context$storage$size(), 1)
  context$driver$expect_visible(&quot;confirmation_message&quot;)
  context
}</pre>
<p>Email notification is different. Submission can succeed even when the email service fails — the two outcomes genuinely can diverge. That’s exactly when a separate step is the right call:</p>
<pre>it(&quot;should submit entry and send notification&quot;, {
  given_no_content() |&gt;
    given_an_authenticated_user(email = &quot;user@example.com&quot;) |&gt;
    given_email_service_is_available() |&gt;
    when_i_submit_entry_with_all_required_fields() |&gt;
    then_entry_is_submitted() |&gt;
    then_notification_was_sent_to_the_authenticated_user()
})</pre>
<p>Two steps, not three — because the spec now reflects the actual structure of the behavior. If <code>then_entry_is_submitted</code> fails, you know the core submission broke. If <code>then_notification_was_sent_to_the_authenticated_user</code> fails, you know the side effect broke. The split carries information because it maps to a real divergence point.</p>
<p>Then steps should also be <strong>read-only</strong>. They inspect state; they don’t change it. A Then step that modifies storage or triggers side effects is doing the wrong job. Keep the flow clean: Given sets up, When acts, Then observes.</p>
<h2 id="making-failure-messages-helpful">Making Failure Messages Helpful</h2>
<p>A failing test with a good message saves minutes. A failing test with a bad message wastes hours.</p>
<p>The most common mistake is letting low-level assertion failures surface directly. When <code>expect_equal(nrow(df), 2)</code> fails with <code>&quot;actual 0, expected 2&quot;</code>, that tells you nothing about which scenario failed or what the data looked like.</p>
<p>The <code>label</code> argument sets the name testthat uses for the <em>object</em> (first argument) in the failure message. Keep it short and descriptive — testthat appends the actual and expected values itself:</p>
<pre>then_there_are_entries &lt;- function(context, n) {
  testthat::expect_equal(
    context$storage$size(), n,
    label = &quot;number of entries in storage&quot;
  )
  context
}</pre>
<p>A failure now reads: <code>&quot;Expected number of entries in storage to equal n&quot;</code> — immediately clear without any manual formatting.</p>
<p>For UI assertions, label the element being checked:</p>
<pre>expect_visible = function(output_id) {
  val &lt;- self$get_value(output = output_id)
  testthat::expect_true(
    !is.null(val) &#038;&#038; nchar(val) &gt; 0,
    label = sprintf(&quot;output '%s'&quot;, output_id)
  )
  invisible(self)
}</pre>
<p>A failure reads: <code>&quot;Expected output 'confirmation_message' to be TRUE&quot;</code> — which immediately tells you which element to look at.</p>
<h2 id="what-doesnt-belong-in-then-steps">What Doesn’t Belong in Then Steps</h2>
<p><strong>Don’t push implementation tests up to Then steps.</strong></p>
<p>If you’re asserting the exact text of an error message, the CSS class applied to an invalid input, or the exact SQL that was executed — those are implementation details. They don’t belong here.</p>
<p>Acceptance-level Then steps answer: did the right thing happen from the user’s perspective?</p>
<h2 id="the-complete-picture">The Complete Picture</h2>
<p>Putting it all together, a well-formed specification has a clear structure at every level:</p>
<pre>describe(&quot;data submission&quot;, {
  it(&quot;should submit entry and send notification&quot;, {
    given_no_content() |&gt;
      given_an_authenticated_user(email = &quot;user@example.com&quot;) |&gt;
      given_email_service_is_available() |&gt;
      when_i_submit_entry_with_all_required_fields() |&gt;
      then_entry_is_submitted() |&gt;
      then_notification_was_sent_to_the_authenticated_user()
  })

  it(&quot;should handle email service failure gracefully&quot;, {
    given_no_content() |&gt;
      given_an_authenticated_user() |&gt;
      given_email_service_is_unavailable() |&gt;
      when_i_submit_entry_with_all_required_fields() |&gt;
      then_entry_is_submitted() |&gt;
      then_i_am_informed_email_was_not_sent()
  })

  it(&quot;should require all required fields&quot;, {
    given_no_content() |&gt;
      given_an_authenticated_user() |&gt;
      when_i_submit_entry_with_missing_required_fields() |&gt;
      then_i_am_prompted_to_provide_required_fields()
  })
})</pre>
<p>Each specification reads like a sentence. None of them mention <code>shinytest2</code>, input IDs, or database queries. The implementation lives behind the DSL, where it can change without touching the specifications.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>Then steps are where a specification earns its credibility.</p>
<p>A specification that only tests easy outcomes produces false confidence. One that asserts user-visible state, system state, and side effects actually catches real problems. Assert outcomes, not implementations. Group assertions by observable behavior and split them only where they can genuinely diverge. Write failure messages that save time. Push implementation details down to unit and module tests.</p>
<p>With Given, When, and Then steps in place, the specifications read like requirements and run on every build. That’s worth more than any testing framework on its own.</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-then"> jakub::sobolewski</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-asserting-outcomes-with-then-steps/">Behavior-Driven Development in R Shiny: Asserting Outcomes with Then Steps</a>]]></content:encoded>
					
		
		<enclosure url="https://jakubsobolewski.com/ogimg-home.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401533</post-id>	</item>
		<item>
		<title>Ghost in the Shell Script</title>
		<link>https://www.r-bloggers.com/2026/05/ghost-in-the-shell-script/</link>
		
		<dc:creator><![CDATA[The Jumping Rivers Blog]]></dc:creator>
		<pubDate>Thu, 28 May 2026 23:59:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://www.jumpingrivers.com/blog/ghost-in-shell-script/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>I’ve been using Unix and Linux systems for nearly 20 years. When I found myself with a Windows laptop for a while I felt lost and quickly installed Windows Subsystem for Linux. And yet the number of commands I know and use regularly is surprisingly short: cd, ls, rm, pwd, ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/ghost-in-the-shell-script/">Ghost in the Shell Script</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://www.jumpingrivers.com/blog/ghost-in-shell-script/"> The Jumping Rivers Blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<p>
<a href = "https://www.jumpingrivers.com/blog/ghost-in-shell-script/">
<img src="https://www.jumpingrivers.com/blog/ghost-in-shell-script/" width="400" style="width:400px" class="image-center" style="display: block; margin: auto;" />
</a>
</p>
<style>
ol li {
margin-bottom: unset;
}
.conversation-block {
background-color: var(--cream);
padding: 1rem;
}
[data-speaker="Claude"] .conversation-speaker::before,
[data-speaker="Tim"] .conversation-speaker::after {
content: '';
display: inline-block;
width: 28px;
height: 28px;
background-size: contain;
background-repeat: no-repeat;
background-color: var(--dark-blue);
border-radius: 25%;
}
[data-speaker="Claude"] .conversation-speaker::before {
background-image: url("clawd.svg");
transform: translateX(-4px) translateY(7px);
}
[data-speaker="Tim"] .conversation-speaker::after {
background-image: url("tim-brock-avatar.webp");
transform: translateX(4px) translateY(7px);
}
.highlight pre {
max-height: 400px;
resize: vertical;
width: fit-content !important;
}
/* https://stackoverflow.com/questions/79134605/resizable-div-with-a-maximum-initial-height */
/* Unset max-height when user drags resizer to allow expansion downwards */
.highlight pre[style*="height"] {
max-height: unset;
}
</style>
<p>I’ve been using Unix and Linux systems for nearly 20 years. When I found myself with a Windows laptop for a while I felt lost and quickly installed Windows Subsystem for Linux. And yet the number of commands I know and use regularly is surprisingly short: <code>cd</code>, <code>ls</code>, <code>rm</code>, <code>pwd</code>, <code>mkdir</code>, <code>source</code>, <code>node</code>, <code>npm</code> and, of course, <code>git</code>. Even then I don’t think I <em>really</em> “know” git.</p>
<p>In his excellent book “Don’t Make Me Think <em>Revisited</em>”, Steve Krug describes how</p>
<blockquote>
<p>Faced with any sort of technology, very few people take the time to read instructions. Instead, we forge ahead and muddle through, making up our own vaguely plausible stories about what we’re doing and why it works.</p>
</blockquote>
<p>Krug’s book is about web and mobile usability but the point he makes here applies across technological domains. And it is safe to say I’ve been “muddling through” with Unix. If you asked me whether something was a Unix command or part of the shell I’d probably just shrug and say something like <a href="https://www.youtube.com/watch?v=N68rAkmqA_A" rel="nofollow" target="_blank">“I… I don’t know that!?&#8221;</a>.</p>
<p>Until recently StackOverflow had always been my crutch to fall back on. From the frequency with which it appears in answers, I know that <code>find</code> is a very useful and versatile command. However, the odds of me remembering how to use it without looking it up are indistinguishable from 0.</p>
<p>But now we have alternatives in the form of various large language models or, to be more precise, multi-modal AI Assistants (like Claude and ChatGPT) and Agents (like Claude Code and Codex). I’d describe myself as AI-agnostic: I see benefits, I worry about issues. So it was with some (I’d say healthy) degree of scepticism I started using Claude to assist me in writing some shell functions to automate tasks I do or would like to do regularly.</p>
<p>I say “I” wrote. Reality is of course that I asked Claude to write and rewrite things for me and then I questioned why it did what it did. This probably makes it sound like I was assessing Claude for a job but it’s actually that I want to understand what has been written and, as we’ve already covered, I am not very good at this stuff.</p>
<h2 id="task-one-fixing-a-common-git-mistake">Task One: fixing a common <code>git</code> mistake</h2>
<p>One of my most common slip-ups with git is to commit changes to one branch (usually main) that were meant to go on a new feature branch. I know <em>a</em> way to recover from this: create the new branch I meant to create all along; do an interactive rebase to remove the commit from main; switch to the new feature branch I just created. There’s not that many steps to the process but it’s still a bit of a faff. Let’s ask Claude:</p>
<div class="conversation-block">
<style>
.conversation-turn {
padding: 0.5rem 1rem;
margin-bottom: 1rem;
border: 1px solid var(--burgundy);
}
.conversation-turn--right {
color: var(--off-white);
background-color: var(--burgundy);
margin-left: 2rem;
border-radius: 1.1rem 1.1rem 0.2rem 1.1rem;
}
.conversation-turn--left {
color: var(--dark-blue);
background-color: var(--off-white);
margin-right: 2rem;
border-radius: 1.1rem 1.1rem 1.1rem 0.2rem;
}
.conversation-turn p {
margin: 0.25rem 0;
}
.conversation-turn .conversation-speaker {
width: 100%;
font-weight: 500;
margin-bottom: 0.25rem;
}
.conversation-turn--right .conversation-speaker {
text-align: right;
}
.conversation-turn--right .conversation-body {
margin-right: 1rem;
}
.conversation-turn--left .conversation-body {
margin-left: 1rem;
}
</style>
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">If you git [commit] to a wrong branch, is there a quick way to move the change to a new branch and remove it from the last commit from the current branch?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body"><p>Yes — assuming you haven’t pushed yet:</p>
<pre># Create and switch to the correct branch from current state
git switch -c correct-branch

# Go back to the wrong branch and undo the commit (keeps changes staged)
git switch wrong-branch
git reset --soft HEAD~1

# Discard the changes
git restore --staged .
git restore .
</pre><p>Or if you want to keep it even simpler — if the commit is only on the wrong branch and not pushed:</p>
<pre>git switch -c correct-branch
git switch wrong-branch
git reset --hard HEAD~1
</pre><p>The switch -c creates the new branch pointing at the same commit, so your work is preserved there. Then you just strip it off the wrong branch with reset –hard.</p>
</div>
</div>
<script>
</script>
</div>
<p>Here we hit a couple of issues.</p>
<ol>
<li>Neither of Claude’s suggestions did the final step of what I wanted: moving me back to the new, correct, branch. This is entirely on me. I forgot to ask (and didn’t notice for a bit). I do that kind of thing quite a lot.</li>
<li>Even for simple tasks, Claude’s responses can be quite verbose.</li>
</ol>
<p>The problem here is not a practical one regarding the use of Claude or an alternative agent, the real problem is it’s quite difficult to write a blog post of a sensible length that actually goes anywhere while accurately recounting the conversation you have with Claude. So what follows is a paraphrase rather than a transcript; shortened for the sake of your sanity and mine.</p>
<div class="conversation-block">
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Can I use git switch &#8211; instead?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body">Yes, and it’s cleaner. <code>git switch -</code> saves you typing the wrong branch name out explicitly.</div>
</div>
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Seems like the sort of thing I should make into a Bash function</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body"><p>Agreed. Something like this (with an added safety check):</p>
<pre>git-move-last() {
 if [ -z &quot;$1&quot; ]; then
 echo &quot;Usage: git-move-last &lt;new-branch-name&gt;&quot;
 return 1
 fi
 git switch -c &quot;$1&quot;
 git switch -
 git reset --hard HEAD~1
}
</pre>
</div>
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">I’ve gone for git-behead. If I wanted to move to the new branch at the end I just add another git switch -?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body"><p>Ha <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1fa93.png" alt="🪓" class="wp-smiley" style="height: 1em; max-height: 1em;" />.</p>
<p>Exactly:</p>
<pre>git-behead() {
 if [ -z &quot;$1&quot; ]; then
 echo &quot;Usage: git-behead &lt;new-branch-name&gt;&quot;
 return 1
 fi
 git switch -c &quot;$1&quot;
 git switch -
 git reset --hard HEAD~1
 git switch -
}
</pre>
</div>
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Ok, one more idea. Suppose I haven’t been looking what I’m doing and I want to remove more than just the last commit to a new branch. Can we recreate the above but with an optional commit count that defaults to 1?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<pre>git-behead() {
 if [ -z &quot;$1&quot; ]; then
 echo &quot;Usage: git-behead &lt;new-branch-name&gt; [commit-count]&quot;
 return 1
 fi
 local count=&quot;${2:-1}&quot;
 git switch -c &quot;$1&quot;
 git switch -
 git reset --hard HEAD~&quot;$count&quot;
 git switch -
}
</pre>
</div>
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Could you comment anything you think isn’t self-explanatory?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<pre>git-behead() {
 if [ -z &quot;$1&quot; ]; then
 echo &quot;Usage: git-behead &lt;new-branch-name&gt; [commit-count]&quot;
 return 1
 fi
 local count=&quot;${2:-1}&quot; # default to 1 commit if not specified
 git switch -c &quot;$1&quot; # create new branch at current HEAD
 git switch - # switch back to original branch
 git reset --hard HEAD~&quot;$count&quot; # remove commits from original branch
 git switch - # switch to new branch
}
</pre>
</div>
</div>
<p>Putting this in my .bashrc file gives me a simple command that I can invoke whenever I make the mistake I regularly make and an optional argument to use whenever I haven’t been paying attention to the branch I’m on for an extended period. It’s also short enough that I think I follow every line, despite my limited knowledge of Bash, and there are comments there for when future me has forgotten. This conversation took about fifteen minutes and included more questions than I’ve included here, plus time I spent running various ad-hoc tests with the different iterations of the function. That seems like a pretty efficient use of my time.</p>
<h2 id="task-two-listing-image-files">Task Two: listing image files</h2>
<p>I do a lot of data visualisation, I look after the artwork for this website and I take photographs for our conferences. In short, I deal with a lot of images in various formats. It would be nice to be able to list only images and their key properties in a specific location with a simple function. Let’s ask Claude:</p>
<div class="conversation-block">
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Can you write me a Bash script for displaying a list of images in a directory that shows file name, width, height and size (in kB) and nothing else?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body"><p>Here’s a clean script using identify from ImageMagick (widely available) with a du fallback for file size:</p>
<pre>#!/usr/bin/env bash

DIR=&quot;${1:-.}&quot;

if ! command -v identify &&gt;/dev/null; then
 echo &quot;Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)&quot; &gt;&2
 exit 1
fi

shopt -s nullglob nocaseglob
files=(&quot;$DIR&quot;/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif})
shopt -u nullglob nocaseglob

if [[ ${#files[@]} -eq 0 ]]; then
 echo &quot;No images found in: $DIR&quot; &gt;&2
 exit 0
fi

printf &quot;%-40s %8s %8s %10s\n&quot; &quot;FILENAME&quot; &quot;WIDTH&quot; &quot;HEIGHT&quot; &quot;SIZE (kB)&quot;
printf &quot;%-40s %8s %8s %10s\n&quot; &quot;--------&quot; &quot;-----&quot; &quot;------&quot; &quot;---------&quot;

for f in &quot;${files[@]}&quot;; do
 name=&quot;$(basename &quot;$f&quot;)&quot;
 read -r width height &lt; &lt;(identify -format &quot;%w %h&quot; &quot;$f&quot; 2&gt;/dev/null | head -1)
 size_kb=$(awk &quot;BEGIN { printf \&quot;%.1f\&quot;, $(wc -c &lt; &quot;$f&quot;) / 1024 }&quot;)
 printf &quot;%-40s %8s %8s %10s\n&quot; &quot;$name&quot; &quot;$width&quot; &quot;$height&quot; &quot;$size_kb&quot;
done
</pre><p>Supported formats: jpg, jpeg, png, gif, webp, bmp, tiff, avif.</p>
<p>Example output:</p>
<pre>FILENAME WIDTH HEIGHT SIZE (kB)
-------- ----- ------ ---------
banner.png 1200 630 84.3
avatar.jpg 400 400 32.1
hero.webp 1920 1080 210.7
</pre></div>
</div>
</div>
<p>I asked for a script, I got a script. I wanted a function. This one is, again, on me.</p>
<div class="conversation-block">
<div class="conversation-turn conversation-turn--tim conversation-turn--right" data-speaker="Tim">
<div class="conversation-speaker">Tim</div>
<div class="conversation-body">Sorry, can we make this a function rather than a script?</div>
</div>
<div class="conversation-turn conversation-turn--claude conversation-turn--left" data-speaker="Claude">
<div class="conversation-speaker">Claude</div>
<div class="conversation-body"><p>The key changes: shebang removed, exit replaced with return, and variables are now local. To use it, source it in your shell and call it as normal:</p>
<pre>imgls() {
 local dir=&quot;${1:-.}&quot;

 if ! command -v identify &&gt;/dev/null; then
 echo &quot;Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)&quot; &gt;&2
 return 1
 fi

 shopt -s nullglob nocaseglob
 local files=(&quot;$dir&quot;/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif})
 shopt -u nullglob nocaseglob

 if [[ ${#files[@]} -eq 0 ]]; then
 echo &quot;No images found in: $dir&quot; &gt;&2
 return 0
 fi

 printf &quot;%-40s %8s %8s %10s\n&quot; &quot;FILENAME&quot; &quot;WIDTH&quot; &quot;HEIGHT&quot; &quot;SIZE (kB)&quot;
 printf &quot;%-40s %8s %8s %10s\n&quot; &quot;--------&quot; &quot;-----&quot; &quot;------&quot; &quot;---------&quot;

 local f name width height size_kb
 for f in &quot;${files[@]}&quot;; do
 name=&quot;$(basename &quot;$f&quot;)&quot;
 read -r width height &lt; &lt;(identify -format &quot;%w %h&quot; &quot;$f&quot; 2&gt;/dev/null | head -1)
 size_kb=$(awk &quot;BEGIN { printf \&quot;%.1f\&quot;, $(wc -c &lt; &quot;$f&quot;) / 1024 }&quot;)
 printf &quot;%-40s %8s %8s %10s\n&quot; &quot;$name&quot; &quot;$width&quot; &quot;$height&quot; &quot;$size_kb&quot;
 done
}
</pre>
</div>
</div>
<p>The conversation goes on a bit from here and the function becomes more and more detailed. It would be tedious to show the whole conversation but to summarise, “we”:</p>
<ol>
<li>Add support for SVG, including extracting dimensions from the <code>viewBox</code> attribute.</li>
<li>Fix a bug with GIF dimensions.</li>
<li>Tweak the header text.</li>
<li>Add support for recursive searching of a directory.</li>
<li>Fix the layout when doing a recursive search.</li>
<li>Round the values in the SIZE column</li>
<li>Add comments.</li>
</ol>
<p>The final result:</p>
<pre>imgls() {
 local recursive=0

 # Parse flags — currently only -r (recursive) is supported
 while getopts &quot;:r&quot; opt; do
 case $opt in
 r) recursive=1 ;;
 *) echo &quot;Usage: imgls [-r] [directory]&quot; &gt;&2; return 1 ;;
 esac
 done
 # Shift past any parsed flags so $1 is the directory argument
 shift $((OPTIND - 1))
 OPTIND=1

 # Use first argument as directory, default to current directory if omitted
 local dir=&quot;${1:-.}&quot;

 # identify is provided by imagemagick and used to read raster image dimensions
 if ! command -v identify &&gt;/dev/null; then
 echo &quot;Error: imagemagick is required (install with: brew install imagemagick / apt install imagemagick)&quot; &gt;&2
 return 1
 fi

 local exts=(&quot;jpg&quot; &quot;jpeg&quot; &quot;png&quot; &quot;gif&quot; &quot;webp&quot; &quot;bmp&quot; &quot;tiff&quot; &quot;tif&quot; &quot;avif&quot; &quot;svg&quot;)
 local files=()

 if [[ $recursive -eq 1 ]]; then
 # Build a find command matching all extensions, case-insensitively
 local find_args=(&quot;$dir&quot; &quot;-type&quot; &quot;f&quot; &quot;(&quot;)
 for i in &quot;${!exts[@]}&quot;; do
 [[ $i -gt 0 ]] && find_args+=(&quot;-o&quot;)
 find_args+=(&quot;-iname&quot; &quot;*.${exts[$i]}&quot;)
 done
 find_args+=(&quot;)&quot;)
 while IFS= read -r f; do
 files+=(&quot;$f&quot;)
 done &lt; &lt;(find &quot;${find_args[@]}&quot; | sort)
 else
 # nullglob: unmatched globs expand to nothing instead of being treated as literals
 # nocaseglob: match extensions case-insensitively (e.g. .JPG as well as .jpg)
 shopt -s nullglob nocaseglob
 files=(&quot;$dir&quot;/*.{jpg,jpeg,png,gif,webp,bmp,tiff,tif,avif,svg})
 shopt -u nullglob nocaseglob
 fi

 if [[ ${#files[@]} -eq 0 ]]; then
 echo &quot;No images found in: $dir&quot; &gt;&2
 return 0
 fi

 # In recursive mode show the relative path, otherwise just the filename
 printf &quot;%-40s %11s %12s %10s\n&quot; &quot;FILENAME&quot; &quot;WIDTH (px)&quot; &quot;HEIGHT (px)&quot; &quot;SIZE (kB)&quot;
 printf &quot;%-40s %11s %12s %10s\n&quot; &quot;--------&quot; &quot;----------&quot; &quot;-----------&quot; &quot;---------&quot;

 local f name width height size_kb viewbox current_subdir=&quot;&quot; subdir
 for f in &quot;${files[@]}&quot;; do
 if [[ $recursive -eq 1 ]]; then
 # Print a header line when we enter a new subdirectory
 subdir=&quot;$(dirname &quot;${f#$dir/}&quot;)&quot;
 if [[ &quot;$subdir&quot; != &quot;$current_subdir&quot; ]]; then
 [[ -n &quot;$current_subdir&quot; ]] && echo &quot;&quot;
 echo &quot;${subdir}/&quot;
 current_subdir=&quot;$subdir&quot;
 fi
 name=&quot;$(basename &quot;$f&quot;)&quot;
 else
 name=&quot;$(basename &quot;$f&quot;)&quot;
 fi
 # wc -c counts raw bytes; awk converts to kB with one decimal place
 size_kb=$(awk &quot;BEGIN { printf \&quot;%d\&quot;, $(wc -c &lt; &quot;$f&quot;) / 1024 }&quot;)

 if [[ &quot;${f,,}&quot; == *.svg ]]; then
 # SVGs are XML so we can parse dimensions directly without imagemagick.
 # Prefer viewBox (e.g. &quot;0 0 100 200&quot;) as it&#39;s the most reliable source
 # of the intended coordinate space; fall back to width/height attributes.
 viewbox=$(grep -oi &#39;viewBox=&quot;[^&quot;]*&quot;&#39; &quot;$f&quot; 2&gt;/dev/null | head -1 | grep -oi &#39;&quot;[^&quot;]*&quot;&#39; | tr -d &#39;&quot;&#39;)
 if [[ -n &quot;$viewbox&quot; ]]; then
 # viewBox is &quot;min-x min-y width height&quot; — fields 3 and 4
 width=$(echo &quot;$viewbox&quot; | awk &#39;{printf &quot;%g&quot;, $3}&#39;)
 height=$(echo &quot;$viewbox&quot; | awk &#39;{printf &quot;%g&quot;, $4}&#39;)
 else
 width=$(grep -oi &#39;width=&quot;[^&quot;]*&quot;&#39; &quot;$f&quot; 2&gt;/dev/null | head -1 | grep -oi &#39;&quot;[^&quot;]*&quot;&#39; | tr -d &#39;&quot;px&quot;&#39;)
 height=$(grep -oi &#39;height=&quot;[^&quot;]*&quot;&#39; &quot;$f&quot; 2&gt;/dev/null | head -1 | grep -oi &#39;&quot;[^&quot;]*&quot;&#39; | tr -d &#39;&quot;px&quot;&#39;)
 fi
 # ${var:--} expands to &quot;-&quot; if var is empty or unset
 width=&quot;${width:--}&quot;
 height=&quot;${height:--}&quot;
 else
 # [0] selects only the first frame, preventing animated GIFs from
 # producing one line of output per frame
 read -r width height &lt; &lt;(identify -format &quot;%w %h&quot; &quot;${f}[0]&quot; 2&gt;/dev/null)
 fi

 printf &quot;%-40s %11s %12s %10s\n&quot; &quot;$name&quot; &quot;$width&quot; &quot;$height&quot; &quot;$size_kb&quot;
 done
}
</pre><p>The “development time” here was longer and I’d be fibbing if I said I understood every line of <em>this</em> function. But I do get the general principles and understand what it can and can’t do. And being able to list all images (of nearly all relevant types) in a directory with just their key properties in a nice tabular format is a very nice productivity win for me.</p>
<h2 id="final-thoughts">Final Thoughts</h2>
<p>Discussions about AI easily tend towards the extremes: ‘it’s amazing’, ‘it’s a disaster’, ‘it’s useless’. This is particularly unhelpful when focusing on how successfully or otherwise it executes a single task. All we’ve really done in this post is create a couple of Bash scripts. For me the AI in question, Claude, has been use<em>ful</em>. No more, no less. Despite this, the process did make me think a lot about how the way we interact with machines is changing. While it’s easy to over-personify AI, this line from the film that gave this blog post its title seems now, in 2026, to have been prescient:</p>
<figure>
<blockquote>
<p>The advent of computers, and the subsequent accumulation of incalculable data has given rise to a new system of memory and thought parallel to your own.</p>
</blockquote>
<figcaption>— Puppet Master, <i>Ghost in the Shell (1995)</i></figcaption>
</figure>
<p>
For updates and revisions to this article, see the <a href = "https://www.jumpingrivers.com/blog/ghost-in-shell-script/">original post</a>
</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://www.jumpingrivers.com/blog/ghost-in-shell-script/"> The Jumping Rivers Blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/ghost-in-the-shell-script/">Ghost in the Shell Script</a>]]></content:encoded>
					
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">401507</post-id>	</item>
		<item>
		<title>rOpenSci News Digest, May 2026</title>
		<link>https://www.r-bloggers.com/2026/05/ropensci-news-digest-may-2026/</link>
		
		<dc:creator><![CDATA[rOpenSci]]></dc:creator>
		<pubDate>Thu, 28 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://ropensci.org/blog/2026/05/28/news-may-2026/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Dear rOpenSci friends, it’s time for our monthly news roundup!  You can read this post on our blog. Now let’s dive into the activity at and around rOpenSci!</p>
<p>rOpenSci HQ</p>
<p>15 Years of rOpenSci, and we’re just getting started 🎉<br />
This y...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/ropensci-news-digest-may-2026/">rOpenSci News Digest, May 2026</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://ropensci.org/blog/2026/05/28/news-may-2026/"> rOpenSci - open tools for open science</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>

<!-- Before sending DELETE THE INDEX_CACHE and re-knit! -->
<p>Dear rOpenSci friends, it’s time for our monthly news roundup! <!-- blabla --> You can read this post <a href="https://ropensci.org/blog/2026/05/28/news-may-2026" rel="nofollow" target="_blank">on our blog</a>. Now let’s dive into the activity at and around rOpenSci!</p>
<h2>
rOpenSci HQ
</h2><h3>
15 Years of rOpenSci, and we’re just getting started <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" />
</h3><p>This year we celebrate 15 years of rOpenSci! First up we have a coworking session followed by a couple casual virtual community celebrations. Please join us! Read more in our <a href="https://ropensci.org/blog/2026/05/13/anniversary2026/" rel="nofollow" target="_blank">blog post</a> about these upcoming events.</p>
<p>Also exciting is that this blog post about our 15 year anniversary was featured in the R Weekly Highlights <a href="https://serve.podhome.fm/episodepage/r-weekly-highlights/227" rel="nofollow" target="_blank">podcast</a> hosted by Eric Nantz and Mike Thomas.</p>
<h3>
Champions Program update
</h3><p>The rOpenSci Champions Program is currently running two very active cohorts, with many activities happening across the program, and everything is on track!</p>
<p>The <strong>2025–2026 cohort</strong> is wrapping up their projects and outreach activities, with several Champions presenting their work at events such as R/Pharma, useR!, Posit, and LatinR 2026. We also recently held a cohort meeting bringing together Champions and mentors from the 2025–2026 cohort and one meeting for mentors only, to share mentoring tips and tricks.</p>
<p>Meanwhile, the <strong>2026–2027 cohort</strong> has started their training activities, including three weekly training workshops for Champions and a mentor training workshop. Mentors and Champions have also had their first meetings together to get to know each other and begin planning their collaboration.</p>
<p>Stay tuned to our <a href="https://ropensci.org/events" rel="nofollow" target="_blank">events page</a> for Champions outreach activities and to our blog to learn more about Champions’ projects.</p>
<h3>
May is Maintainer Month
</h3><p>Open source software doesn’t sustain itself. Behind every R package, there is at least one person who responds to issues, reviews pull requests, keeps up with dependency changes, and makes sure everything still works.</p>
<p>We joined the #MaintainerMonth celebration with a month-long series of rOpenSci’s maintainer spotlights: <strong>36 maintainers from 15 countries across 4 continents</strong>, maintaining more than 50 packages that together serve thousands of researchers and data practitioners around the world.</p>
<p>Follow our social media (<a href="https://hachyderm.io/@rOpenSci" rel="nofollow" target="_blank">Mastodon</a> and <a href="https://www.linkedin.com/company/ropensci/" rel="nofollow" target="_blank">LinkedIn</a>) and the official hashtag (#MaintainerMonth) to learn more about the maintainers who support open source software. <strong>Thank you</strong> to those who agreed to participate and to all the maintainers in our community.</p>
<h3>
Software-Review update
</h3><p>This month saw a slight policy shift in the way editors handle new software review submissions, assisted by an internal update to our bot system. Previously, our rotating Editor-in-Chief (EiC) was solely responsible for initial decisions on whether to proceed with submissions. Once a decision was reached, a handling editor took over, and generally guided each submission through to approval. However, recent increases in submissions have been too much for one EiC to handle. Submissions will now be passed on more quickly to handling editors who will then make initial scope and fit decisions.</p>
<p>As part of this change, the EiC can now ask the bot to “ping editors”. Editors will receive an email alerting them of this new submission and can then decide whether to take it on as editor. Each time an editor clicks on a link in the email, the EiC will receive an email notification, and once an editor is assigned, all links will be automatically deactivated.</p>
<p>For anybody interested in how to develop an email alert system like this, please contact us and we’ll have happy to share what we learnt along the way. Full code is in <a href="https://github.com/ropensci-review-tools/roreviewapi/blob/main/R/editor-search.R" rel="nofollow" target="_blank">this R file</a>.</p>
<h3>
Social media cards for R-universe
</h3><p>When posting about your package or universe on social media, you can get a pretty social media card featuring important information about (respectively):</p>
<ul>
<li>the package such as its maintainer, its <a href="https://docs.r-universe.dev/publish/metadata.html#keywords" rel="nofollow" target="_blank">keywords</a>, its URL, its version, its number of stars, etc.</li>
</ul>
<figure class="center"><img src="https://i1.wp.com/ropensci.r-universe.dev/targets/card.png?w=450&#038;ssl=1"  data-recalc-dims="1">
</figure>
<ul>
<li>the universe such as its number of packages and contributors, etc.</li>
</ul>
<figure class="center"><img src="https://i1.wp.com/ropensci.r-universe.dev/card.png?w=450&#038;ssl=1"  data-recalc-dims="1">
</figure>
<p><a href="https://docs.r-universe.dev/browse/packages.html#social-media-cards" rel="nofollow" target="_blank">More information in the R-universe documentation</a>.</p>
<h3>
Coworking
</h3><p>Read <a href="https://ropensci.org/blog/2023/06/21/coworking/" rel="nofollow" target="_blank">all about coworking</a>!</p>
<ul>
<li>Tuesday June 2nd 2026, 14:00 Europe Central (12:00 UTC) <a href="https://ropensci.org/events/coworking-2026-06/" rel="nofollow" target="_blank">“15 years with rOpenSci”</a>, with <a href="https://ropensci.org/author/steffi-lazerte/" rel="nofollow" target="_blank">Steffi LaZerte</a> and cohost <a href="https://ropensci.org/author/noam-ross" rel="nofollow" target="_blank">Noam Ross</a>.
<ul>
<li>Explore rOpenSci projects</li>
<li>Do a deep dive into the rOpenSci blog history</li>
<li>Meet rOpenSci’s Executive Director, Noam Ross, and discuss how rOpenSci has evolved over the last 15 years.</li>
</ul>
</li>
<li>Tuesday July 7nd 2026, 09:00 Americas Pacific (16:00 UTC) <a href="https://ropensci.org/events/coworking-2026-07/" rel="nofollow" target="_blank">“Debugging in R”</a>, with <a href="https://ropensci.org/author/yanina-bellini-saibene/" rel="nofollow" target="_blank">Yanina Bellini Saibene</a> and cohost <a href="https://ropensci.org/author/shannon-pileggi" rel="nofollow" target="_blank">Shannon Pileggi</a>.
<ul>
<li>Read up on debugging in R</li>
<li>Meet community host, Shannon Pileggi, and discuss tips and tricks for debugging in R.</li>
</ul>
</li>
</ul>
<p>And remember, you can always cowork independently on work related to R, work on packages that tend to be neglected, or work on what ever you need to get done!</p>
<h2>
Software <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" />
</h2><h3>
New versions
</h3><p>The following ten packages have had an update since the last newsletter: <a href="https://docs.ropensci.org/cffr" title="Generate Citation File Format (CFF) Metadata for R Packages" rel="nofollow" target="_blank">cffr</a> (<a href="https://github.com/ropensci/cffr/releases/tag/v1.4.0" rel="nofollow" target="_blank"><code>v1.4.0</code></a>), <a href="https://docs.ropensci.org/gutenbergr" title="Download and Process Public Domain Works from Project Gutenberg" rel="nofollow" target="_blank">gutenbergr</a> (<a href="https://github.com/ropensci/gutenbergr/releases/tag/v0.5.1" rel="nofollow" target="_blank"><code>v0.5.1</code></a>), <a href="https://docs.ropensci.org/dataset" title="Create Data Frames for Exchange and Reuse" rel="nofollow" target="_blank">dataset</a> (<a href="https://github.com/ropensci/dataset/releases/tag/0.4.4" rel="nofollow" target="_blank"><code>0.4.4</code></a>), <a href="https://docs.ropensci.org/sofa" title="Connector to CouchDB" rel="nofollow" target="_blank">sofa</a> (<a href="https://github.com/ropensci/sofa/releases/tag/v0.4.1" rel="nofollow" target="_blank"><code>v0.4.1</code></a>), <a href="https://docs.ropensci.org/saperlipopette" title="Create Example Git Messes" rel="nofollow" target="_blank">saperlipopette</a> (<a href="https://github.com/ropensci-training/saperlipopette/releases/tag/v1.0.0" rel="nofollow" target="_blank"><code>v1.0.0</code></a>), <a href="https://docs.ropensci.org/medrxivr" title="Access and Search MedRxiv and BioRxiv Preprint Data" rel="nofollow" target="_blank">medrxivr</a> (<a href="https://github.com/ropensci/medrxivr/releases/tag/snapshot" rel="nofollow" target="_blank"><code>snapshot</code></a>), <a href="https://docs.ropensci.org/textreuse" title="Detect Text Reuse and Document Similarity" rel="nofollow" target="_blank">textreuse</a> (<a href="https://github.com/ropensci/textreuse/releases/tag/v1.0.1" rel="nofollow" target="_blank"><code>v1.0.1</code></a>), <a href="https://docs.ropensci.org/EDIutils" title="An API Client for the Environmental Data Initiative Repository" rel="nofollow" target="_blank">EDIutils</a> (<a href="https://github.com/ropensci/EDIutils/releases/tag/v2.1.0" rel="nofollow" target="_blank"><code>v2.1.0</code></a>), <a href="https://docs.ropensci.org/rsi" title="Efficiently Retrieve and Process Satellite Imagery" rel="nofollow" target="_blank">rsi</a> (<a href="https://github.com/Permian-Global-Research/rsi/releases/tag/v0.3.3" rel="nofollow" target="_blank"><code>v0.3.3</code></a>), and <a href="https://docs.ropensci.org/UCSCXenaTools" title="Download and Explore Datasets from UCSC Xena Data Hubs" rel="nofollow" target="_blank">UCSCXenaTools</a> (<a href="https://github.com/ropensci/UCSCXenaTools/releases/tag/v1.7.0" rel="nofollow" target="_blank"><code>v1.7.0</code></a>).</p>
<p>The writexl package has a <a href="https://github.com/ropensci/writexl/pull/98#issuecomment-4191858158" rel="nofollow" target="_blank">new maintainer</a>, Bill Denney. NLMR is now maintained by <a href="https://github.com/ropensci/NLMR/issues/116#issuecomment-4280937012" rel="nofollow" target="_blank">Jakub Nowosad</a>.</p>
<h2>
Software Peer Review
</h2><p>There are eighteen recently closed and active submissions and 4 submissions on hold. Issues are at different stages:</p>
<ul>
<li>
<p>One at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%226/approved%22" rel="nofollow" target="_blank">‘6/approved’</a>:</p>
<ul>
<li><a href="https://github.com/ropensci/software-review/issues/754" rel="nofollow" target="_blank">saperlipopette</a>, Create Example Git Messes. Submitted by <a href="https://masalmon.eu/" rel="nofollow" target="_blank">Maëlle Salmon</a>.</li>
</ul>
</li>
<li>
<p>Three at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%225/awaiting-reviewer(s)-response%22" rel="nofollow" target="_blank">‘5/awaiting-reviewer(s)-response’</a>:</p>
<ul>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/750" rel="nofollow" target="_blank">nycOpenData</a>, Convenient Access to NYC Open Data API Endpoints. Submitted by <a href="https://github.com/martinezc1" rel="nofollow" target="_blank">Christian Martinez</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/730" rel="nofollow" target="_blank">ernest</a>, A Toolkit for Nested Sampling. Submitted by <a href="https://github.com/kylesnap" rel="nofollow" target="_blank">Kyle Dewsnap</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/671" rel="nofollow" target="_blank">pkgmatch</a>, Find R Packages Matching Either Descriptions or Other R Packages. Submitted by <a href="https://mpadge.github.io/" rel="nofollow" target="_blank">mark padgham</a>.</p>
</li>
</ul>
</li>
<li>
<p>Six at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%224/review(s)-in-awaiting-changes%22" rel="nofollow" target="_blank">‘4/review(s)-in-awaiting-changes’</a>:</p>
<ul>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/760" rel="nofollow" target="_blank">pvEBayes</a>, Empirical Bayes Methods for Pharmacovigilance. Submitted by <a href="https://github.com/YihaoTancn" rel="nofollow" target="_blank">Yihao Tan</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/743" rel="nofollow" target="_blank">RAMEN</a>, RAMEN: Regional Association of Methylome variability with the Exposome and geNome. Submitted by <a href="https://erick-navarrodelgado.netlify.app/" rel="nofollow" target="_blank">Erick Navarro-Delgado</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/741" rel="nofollow" target="_blank">logolink</a>, An Interface for Running NetLogo Simulations. Submitted by <a href="http://danielvartan.com/" rel="nofollow" target="_blank">Daniel Vartanian</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/718" rel="nofollow" target="_blank">rcrisp</a>, Automate the Delineation of Urban River Spaces. Submitted by <a href="https://github.com/cforgaci" rel="nofollow" target="_blank">Claudiu Forgaci</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/704" rel="nofollow" target="_blank">priorsense</a>, Prior Diagnostics and Sensitivity Analysis. Submitted by <a href="https://github.com/n-kall" rel="nofollow" target="_blank">Noa Kallioinen</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/615" rel="nofollow" target="_blank">galamm</a>, Generalized Additive Latent and Mixed Models. Submitted by <a href="https://osorensen.github.io/" rel="nofollow" target="_blank">Øystein Sørensen</a>. (Stats).</p>
</li>
</ul>
</li>
<li>
<p>One at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%223/reviewer(s)-assigned%22" rel="nofollow" target="_blank">‘3/reviewer(s)-assigned’</a>:</p>
<ul>
<li><a href="https://github.com/ropensci/software-review/issues/765" rel="nofollow" target="_blank">ciecl</a>, International Classification of Diseases ICD-10/ICD-11 for Chile. Submitted by <a href="https://github.com/Rodotasso" rel="nofollow" target="_blank">Rodolfo Tasso</a>.</li>
</ul>
</li>
<li>
<p>Five at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%222/seeking-reviewer(s)%22" rel="nofollow" target="_blank">‘2/seeking-reviewer(s)’</a>:</p>
<ul>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/763" rel="nofollow" target="_blank">EpiStrainDynamics</a>, Infer temporal trends of multiple pathogens. Submitted by <a href="http://www.smwindecker.com/" rel="nofollow" target="_blank">Saras Windecker</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/762" rel="nofollow" target="_blank">lakefetch</a>, Calculate Fetch and Wave Exposure for Lake Sampling Points. Submitted by <a href="https://github.com/jeremylfarrell" rel="nofollow" target="_blank">jeremylfarrell</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/744" rel="nofollow" target="_blank">RAQSAPI</a>, A Simple Interface to the US EPA Air Quality System Data Mart API. Submitted by <a href="https://github.com/mccroweyclinton-EPA" rel="nofollow" target="_blank">mccroweyclinton-EPA</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/740" rel="nofollow" target="_blank">fcmconfr</a>, Fuzzy Cognitive Map Analysis in R. Submitted by <a href="https://github.com/bhroston" rel="nofollow" target="_blank">benroston</a>. (Stats).</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/717" rel="nofollow" target="_blank">coevolve</a>, Fit Bayesian Generalized Dynamic Phylogenetic Models using Stan. Submitted by <a href="https://scottclaessens.github.io/" rel="nofollow" target="_blank">Scott Claessens</a>. (Stats).</p>
</li>
</ul>
</li>
<li>
<p>Two at <a href="https://github.com/ropensci/software-review/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc+label%3A%221/editor-checks%22" rel="nofollow" target="_blank">‘1/editor-checks’</a>:</p>
<ul>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/752" rel="nofollow" target="_blank">metasurvey</a>, Reproducible Survey Data Processing with Step Pipelines. Submitted by <a href="https://github.com/mauroloprete" rel="nofollow" target="_blank">Mauro Loprete</a>.</p>
</li>
<li>
<p><a href="https://github.com/ropensci/software-review/issues/725" rel="nofollow" target="_blank">LBDiscoverAnalysis</a>, Co-occurrence Discovery Models and Visualization for Biomedical LBD. Submitted by <a href="https://github.com/chaoliu-cl" rel="nofollow" target="_blank">Chao Liu</a>.</p>
</li>
</ul>
</li>
</ul>
<p>Find out more about <a href="https://ropensci.org/software-review" rel="nofollow" target="_blank">Software Peer Review</a> and how to get involved.</p>
<h2>
On the blog
</h2><!-- Do not forget to rebase your branch! -->
<ul>
<li>
<p><a href="https://ropensci.org/blog/2026/05/06/mentors-2026" rel="nofollow" target="_blank">New Mentoring Team with Experienced Mentors and New Voices</a> by Alber Hamersson Sánchez Ipia, Pablo Paccioretti, Erick Navarro Delgado, Guadalupe Pascal, Andrea Gomez Vargas, Monika Avila Marquez, Luis D. Verde Arregoitia, Francisco Cardozo, Elio Campitelli, Pao Corrales, and Yanina Bellini Saibene. Meet the new team of mentors who will accompany our Champions in their projects for a year, sharing experience, guidance and enthusiasm. Other languages: <a href='https://ropensci.org/es/blog/2026/05/26/mentoras_es-2026' lang='es' rel="nofollow" target="_blank">Nuevo equipo de mentor(a|e)s con experiencia y nuevas voces (es)</a>.</p>
</li>
<li>
<p><a href="https://ropensci.org/blog/2026/05/13/anniversary2026" rel="nofollow" target="_blank">15 Years of rOpenSci, and We’re Just Getting Started <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f389.png" alt="🎉" class="wp-smiley" style="height: 1em; max-height: 1em;" /></a> by The rOpenSci Team.</p>
</li>
</ul>
<figure class="center"><img src="https://i1.wp.com/ropensci.org/blog/2026/05/28/news-may-2026/PixelArt15yearrOpenSci.png?w=400&#038;ssl=1"
alt="Retro pixel-art graphic celebrating rOpenSci&#39;s 15th anniversary. The text &#39;rOpenSci&#39; appears at the top in pixel font, flanked by three pixel-art balloons. A browser window frames the central message: &#39;15 YEARS / TRANSFORMING OPEN SCIENCE&#39; in bold pixel letters, overlaid on the rOpenSci geometric network pattern. A pixel badge reads &#39;OMG&#39;. A pixel folder and sparkle icons complete the design."  data-recalc-dims="1">
</figure>
<h2>
Calls for contributions
</h2><h3>
Calls for maintainers
</h3><p>If you’re interested in maintaining any of the R packages below, you might enjoy reading our blog post <a href="https://ropensci.org/blog/2023/02/07/what-does-it-mean-to-maintain-a-package/" rel="nofollow" target="_blank">What Does It Mean to Maintain a Package?</a>.</p>
<ul>
<li>
<p><a href="https://docs.ropensci.org/charlatan" rel="nofollow" target="_blank">charlatan</a>, create fake data in R. <a href="https://github.com/ropensci/charlatan/issues/150" rel="nofollow" target="_blank">Issue for volunteering</a>.</p>
</li>
<li>
<p><a href="https://docs.ropensci.org/landscapetools" rel="nofollow" target="_blank">landscapetools</a>, R package for some of the less-glamorous tasks involved in landscape analysis. <a href="https://github.com/ropensci/landscapetools/issues/48" rel="nofollow" target="_blank">Issue for volunteering</a>.</p>
</li>
<li>
<p><a href="https://docs.ropensci.org/hddtools" rel="nofollow" target="_blank">hddtools</a>, Tools to discover hydrological data, accessing catalogues and databases from various data providers. <a href="https://github.com/ropensci/hddtools/issues/36" rel="nofollow" target="_blank">Issue for volunteering</a>.</p>
</li>
</ul>
<h3>
Calls for contributions
</h3><p>Refer to our <a href="https://ropensci.org/help-wanted/" rel="nofollow" target="_blank">help wanted page</a> – before opening a PR, we recommend asking in the issue whether help is still needed.</p>
<h2>
Package development corner
</h2><p>Some useful information for R package developers. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f440.png" alt="👀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h3>
devtools’ install functions now officially deprecated
</h3><p>The <code>devtools::install_</code> functions like <a href="https://devtools.r-lib.org/reference/install-deprecated.html" rel="nofollow" target="_blank"><code>devtools::install_github()</code></a> have now been officially <a href="https://devtools.r-lib.org/reference/install-deprecated.html" rel="nofollow" target="_blank">deprecated</a> in favor of <a href="https://pak.r-lib.org/" rel="nofollow" target="_blank">pak</a>. The <a href="https://devtools.r-lib.org/reference/install-deprecated.html" rel="nofollow" target="_blank">manual page</a> contains a migration guide. Time to update older READMEs!</p>
<h3>
New Git commands!
</h3><p>New in Git: git history! Thanks to Hugo Gruson for bringing this to our attention.</p>
<p>Imagine…</p>
<p><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f630.png" alt="😰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Oops, that old commit’s message had a typo! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f60c.png" alt="😌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> git history reword <commit-id></p>
<p><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f630.png" alt="😰" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Oops, that old commit is too big! <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f60c.png" alt="😌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> git history split <commit-id> (to split it into two commits)</p>
<p><a href="https://github.blog/open-source/git/highlights-from-git-2-54/#h-rewrite-history-with-git-history" rel="nofollow" target="_blank">Git release notes</a>; <a href="https://git-scm.com/docs/git-history" rel="nofollow" target="_blank">Git docs</a>.</p>
<h3>
roxygen2 8.0.0
</h3><p>A new version of roxygen2 hit CRAN. Among the highlights are improvements for R6, S7, inheritance, and better documentation of roxygen2 itself, including the <a href="https://roxygen2.r-lib.org/articles/extending.html" rel="nofollow" target="_blank">vignette on extending roxygen2 with your own tags and roclets</a>. <a href="https://opensource.posit.co/blog/2026-05-01_roxygen2-8-0-0/" rel="nofollow" target="_blank">Release announcement</a>.</p>
<h3>
Comment <code>DESCRIPTION</code> files
</h3><p>Have you noticed this line in the release notes of R 4.6.0?</p>
<blockquote>
<p><a href="https://rdrr.io/r/base/dcf.html" rel="nofollow" target="_blank"><code>read.dcf()</code></a> now recognizes lines starting with <code>#</code> as comment lines. By Dirk Eddelbuettel, Laurent Gatto and Hugo Gruson.</p>
</blockquote>
<p>This means it’s becoming possible to add comments to <code>DESCRIPTION</code> files, for instance to note why you added a given dependency.</p>
<h3>
{cross} R package for running code with different package versions
</h3><p>Have you ever used <a href="https://masalmon.eu/2024/01/23/git-worktree/" rel="nofollow" target="_blank">Git worktree</a> to load several R package versions at once (in different sessions), for instance to create a benchmark? Davis Vaughan made such endeavors much smoother with his <a href="https://github.com/DavisVaughan/cross" rel="nofollow" target="_blank">cross R package</a>. cross uses Git worktree under the hood to install different package versions in temporary libraries and then runs R code with these different versions. Furthermore, it has a user-friendly interface!</p>
<p>For example, to compare the released version of vctrs to the developement version on GitHub, you could run the following.</p>
<pre>cross::bench_versions(pkgs = c(&quot;vctrs&quot;, &quot;r-lib/vctrs&quot;), {
 library(vctrs)
 x &lt;- c(1, NA, 2, 3, NA)
 bench::mark(missing = vec_detect_missing(x))
})
</pre><h3>
On attribution with a footnote
</h3><p>James Balamuta wrote two thoughtful posts on a <a href="https://blog.thecoatlessprofessor.com/posts/closing-the-footnote-loop/" rel="nofollow" target="_blank">tricky situation and its resolution</a>. TL;DR James’ work on portable R was used in another repository with only minimal acknowledgement through a footnote in a comment. However, those involved came together to work out a resolution through meaningful conversation.</p>
<h2>
Last words
</h2><p>Thanks for reading! If you want to get involved with rOpenSci, check out our <a href="https://contributing.ropensci.org/" rel="nofollow" target="_blank">Contributing Guide</a>. This guide will help direct you to the right place, whether you want to make code contributions, non-code contributions, or contribute in other ways such as through sharing use cases. You can also support our work through <a href="https://ropensci.org/donate" rel="nofollow" target="_blank">donations</a>.</p>
<p>If you haven’t subscribed to our newsletter yet, you can <a href="https://ropensci.org/news/" rel="nofollow" target="_blank">do so though our signup form</a>. Until it’s time for our next newsletter, you can keep in touch with us through our <a href="https://ropensci.org/" rel="nofollow" target="_blank">website</a>, <a href="https://hachyderm.io/@rOpenSci" rel="nofollow" target="_blank">Mastodon</a>, or <a href="https://www.linkedin.com/company/ropensci/" rel="nofollow" target="_blank">LinkedIn</a>. See you soon!</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://ropensci.org/blog/2026/05/28/news-may-2026/"> rOpenSci - open tools for open science</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/ropensci-news-digest-may-2026/">rOpenSci News Digest, May 2026</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401509</post-id>	</item>
		<item>
		<title>survivoR now includes US50 and AU12</title>
		<link>https://www.r-bloggers.com/2026/05/survivor-now-includes-us50-and-au12/</link>
		
		<dc:creator><![CDATA[Daniel Oehm]]></dc:creator>
		<pubDate>Wed, 27 May 2026 09:54:21 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://gradientdescending.com/?p=3531</guid>

					<description><![CDATA[<p>After a couple of big seasons survivoR v2.3.12 has been updated with US50 and AU12. Get it on: As of […]<br />
The post survivoR now includes US50 and AU12 appeared first on Dan Oehm &#124; Gradient Descending.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/survivor-now-includes-us50-and-au12/">survivoR now includes US50 and AU12</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="http://gradientdescending.com/survivor-now-includes-us50-and-au12/"> R Archives - Dan Oehm | Gradient Descending</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<a href="http://gradientdescending.com/survivor-now-includes-us50-and-au12/" title="survivoR now includes US50 and AU12" rel="nofollow" target="_blank"><img loading="lazy" src="https://i1.wp.com/gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211-1024x767.png?w=450" class="webfeedsFeaturedVisual wp-post-image" alt="" style="display: block; margin: auto; margin-bottom: 5px;max-width: 100%;" link_thumbnail="1" decoding="async" srcset_temp="https://i1.wp.com/gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211-1024x767.png?w=450 1024w, http://gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211-300x225.png 300w, http://gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211-768x575.png 768w, http://gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211-1536x1151.png 1536w, http://gradientdescending.com/wp-content/uploads/2026/05/Screenshot-2026-05-27-195211.png 1567w" sizes="(max-width: 1024px) 100vw, 1024px" data-recalc-dims="1" /></a>
<p class="wp-block-paragraph">After a couple of big seasons survivoR v2.3.12 has been updated with US50 and AU12.</p>



<p class="wp-block-paragraph">Get it on:</p>



<ul class="wp-block-list">
<li><a href="https://github.com/doehm/survivoR" rel="nofollow" target="_blank">Github</a></li>



<li><a href="https://github.com/doehm/survivoR/raw/refs/heads/master/dev/xlsx/survivoR.xlsx" rel="nofollow" target="_blank">XLSX download</a></li>



<li><a href="https://github.com/doehm/survivoR/tree/master/dev/json" rel="nofollow" target="_blank">JSON endpoints</a></li>
</ul>



<p class="wp-block-paragraph">As of writing, the survivoR package is closing in on 30,000 downloads, which is wild to me. Not only that, but the CRAN installs are probably the lowest volume of all formats. Installs directly from Git and the XLSX downloads are probably much higher. I never thought this data would be so popular.</p>



<p class="wp-block-paragraph">I don’t have much of a social media presence, pretty much only Bluesky these days, and a very small following there. Regardless many people reach out to say thanks for pulling it together. It has enabled them to build their own websites (of which there are quite a few – <a href="https://survivor-reference.com/" rel="nofollow" target="_blank">Survivor-reference</a>, <a href="https://survivorelo.com/" rel="nofollow" target="_blank">survivorELO</a>), be used for research papers, Master theses, school projects, help potential castaways with their Survivor auditions, help future Survivor players prep for their season, even then <a href="https://survivordle.com/" rel="nofollow" target="_blank">Survivordle</a> web app.</p>



<p class="sg-ai-highlighted-block wp-block-paragraph">I enjoy hearing about how others are utilizing it. If you have experience with it as well, please reach out—I am always keen to discuss Survivor!</p>
<p>The post <a href="http://gradientdescending.com/survivor-now-includes-us50-and-au12/" rel="nofollow" target="_blank">survivoR now includes US50 and AU12</a> appeared first on <a href="http://gradientdescending.com/" rel="nofollow" target="_blank">Dan Oehm | Gradient Descending</a>.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="http://gradientdescending.com/survivor-now-includes-us50-and-au12/"> R Archives - Dan Oehm | Gradient Descending</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/survivor-now-includes-us50-and-au12/">survivoR now includes US50 and AU12</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401490</post-id>	</item>
		<item>
		<title>Speeding up Stan model builds for R package developers</title>
		<link>https://www.r-bloggers.com/2026/05/speeding-up-stan-model-builds-for-r-package-developers/</link>
		
		<dc:creator><![CDATA[R &#124; Dr Tom Palmer]]></dc:creator>
		<pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://remlapmot.github.io/post/2026/stan-compile-speedup/</guid>

					<description><![CDATA[<p>Introduction<br />
In my previous job my work computer was a Windows desktop – yes, those were the days before laptops and hotdesking!<br />
My PhD student was interested in Bayesian methods and we put together an R package which included some<br />
Stan models. ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/speeding-up-stan-model-builds-for-r-package-developers/">Speeding up Stan model builds for R package developers</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://remlapmot.github.io/post/2026/stan-compile-speedup/"> R | Dr Tom Palmer</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<h2 id="introduction">Introduction</h2>
<p>In my previous job my work computer was a Windows desktop – yes, those were the days before laptops and hotdesking!</p>
<p>My PhD student was interested in Bayesian methods and we put together an R package which included some 
<a href="https://mc-stan.org/" rel="nofollow" target="_blank">Stan</a> models. I was always frustrated by how slowly these compiled on our Windows machines. A few years later, when I got a MacBook Air I was shocked how much faster they compiled.</p>
<p>On my Windows machine our 
<a href="https://okezie94.github.io/mrbayes/" rel="nofollow" target="_blank">mrbayes</a> package takes 3 minutes 55 seconds to compile and install. On my M4 MacBook Air it takes 1 minute 16 seconds.</p>
<p>The following tips show how to improve those timings.</p>
<p>To generate the timings I used</p>
<pre>time R CMD INSTALL --preclean .
</pre>
<h2 id="big-win-1-enable-parallel-compilations-with-the-makeflags-environment-variable">Big win 1: Enable parallel compilations with the <code>MAKEFLAGS</code> environment variable</h2>
<p>Set the <code>MAKEFLAGS</code> environment variable in your <em>~/.Renviron</em> file. This controls how many <code>make</code> jobs run concurrently. Choose a number no larger than the number of processing cores your machine has. To find this run</p>
<pre># Windows - in a Git Bash shell
echo $NUMBER_OF_PROCESSORS

# macOS
sysctl -n hw.logicalcpu

# Ubuntu Linux
nproc
</pre>
<p>A reasonable starting point is your core count, or a few fewer to leave headroom for whatever else you’re doing during a compilation. For example,</p>
<pre># In ~/.Renviron
MAKEFLAGS=-j6
</pre>
<p>Close and restart R/RStudio after making this change.</p>
<p>On my Windows machine this reduced the build from 3:55 to 1:15. To find your own sweet spot empirically, see the example at the end of Big win 2.</p>
<h2 id="big-win-2-enable-cc-compiler-cache-using-ccache">Big win 2: Enable C/C++ compiler cache using <code>ccache</code></h2>
<p>Install 
<a href="https://ccache.dev/" rel="nofollow" target="_blank"><code>ccache</code></a>, I find it easiest to use a package manager, e.g.,</p>
<pre># macOS
brew install ccache

# Ubuntu/Debian Linux
apt install ccache

# Windows
winget install ccache
</pre>
<p>Whichever installation method you use make sure <code>ccache</code> is on your <code>PATH</code> after installation. You can test with, say,</p>
<pre>ccache --version
</pre>
<p>To enable <code>ccache</code>, on macOS and Linux this goes in <em>~/.R/Makevars</em>; on Windows it’s <em>~/.R/Makevars.win</em> (create the directory and file if they don’t exist), set</p>
<pre># macOS
CC = ccache clang
CXX = ccache clang++
CXX17 = ccache clang++

# Windows and Linux
# Most Linux users will be on gcc by default
# Change to clang if you're using that
CC = ccache gcc
CXX = ccache g++
CXX17 = ccache g++
</pre>
<p>After a first compilation run for the cache to be generated, subsequent compilations are much faster.</p>
<ul>
<li>Windows, second compilation: 18 seconds</li>
<li>M4 MacBook Air, second compilation: 5 seconds</li>
</ul>
<p>Perhaps more importantly, if, say, your package has 5 models and you only amend the code for one of them, <code>ccache</code> knows to use the cache for the 4 unchanged models.</p>
<ul>
<li>Windows, second compilation, only 1 model edited: 1 minute 10 seconds</li>
<li>M4 MacBook Air, second compilation, only 1 model edited: 19 seconds</li>
</ul>
<p>You can verify <code>ccache</code> is working, by observing the timing decrease and by checking the output of</p>
<pre>ccache -s
</pre>
<p>It is also useful to zero the ccache statistics before a timing run with</p>
<pre>ccache -z
</pre>
<h3 id="testing-which-of-your-models-takes-the-longest-to-compile">Testing which of your models takes the longest to compile</h3>
<p>Here’s a quick script to test which model takes the longest to compile. Save it as say <em>test.sh</em> at the top level of your repo and add <code>^test\.sh$</code> to your <em>.Rbuildignore</em> file (to avoid an <code>R CMD check</code> NOTE about unknown files at the top level).</p>
<pre>for model in inst/stan/*.stan; do
  cp &quot;$model&quot; &quot;$model.bak&quot;
  # Insert at the top of the file
  sed -i &quot;1i // benchmark $(date +%s%N)&quot; &quot;$model&quot;
  ccache -z
  SECONDS=0
  R CMD INSTALL --preclean . &gt;/dev/null 2&gt;&1
  echo &quot;$(basename $model): ${SECONDS}s&quot;
  ccache -s | grep -E &quot;Hits|Misses&quot; | head -2
  mv &quot;$model.bak&quot; &quot;$model&quot;
done
</pre>
<h3 id="finding-your-makeflags-sweet-spot">Finding your <code>MAKEFLAGS</code> sweet spot</h3>
<p>With <code>ccache</code> installed you can now benchmark different <code>-jN</code> values cleanly (the <code>ccache -C</code> calls ensure each run is a cold compile, so you measure raw compilation cost rather than cache hits). You can increase the number sequence up to the number of processing cores your machine has.</p>
<pre>for j in 1 2 3 4 6 8 10; do
  ccache -C &gt;/dev/null
  echo &quot;=== -j$j ===&quot;
  SECONDS=0
  MAKEFLAGS=-j$j R CMD INSTALL --preclean . &gt;/dev/null 2&gt;&1
  echo &quot;elapsed: ${SECONDS}s&quot;
done
</pre>
<p>The timings on my MacBook Air were</p>
<pre>=== -j1 ===
elapsed: 76s
=== -j2 ===
elapsed: 48s
=== -j3 ===
elapsed: 35s
=== -j4 ===
elapsed: 36s
=== -j6 ===
elapsed: 27s
=== -j8 ===
elapsed: 27s
=== -j10 ===
elapsed: 28s
</pre>
<p>My MacBook Air has 10 cores, but only 4 of those are performance cores, so I settled on <code>-j6</code> as that is where my timings plateaued — and it leaves headroom for me inevitably checking my email during a compilation.</p>
<h2 id="big-win-3-combining-these-in-github-actions-workflows">Big win 3: Combining these in GitHub Actions workflows</h2>
<p>In my <em>.github/workflows/R-CMD-check.yaml</em> I have steps for these speedups. Firstly, to set <code>MAKEFLAGS</code>.</p>
<pre>      - name: Set parallel compilation flags (Linux and macOS)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          NCPUS=$(nproc 2&gt;/dev/null || sysctl -n hw.logicalcpu)
          echo &quot;Detected ${NCPUS} processors&quot;
          echo &quot;MAKEFLAGS=-j${NCPUS}&quot; &gt;&gt; ~/.Renviron

      - name: Set parallel compilation flags (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          Write-Output &quot;Detected $env:NUMBER_OF_PROCESSORS processors&quot;
          Add-Content -Path &quot;$HOME\.Renviron&quot; -Value &quot;MAKEFLAGS=-j$env:NUMBER_OF_PROCESSORS&quot;
</pre>
<p>You can also use ccache in GitHub Actions, as follows:</p>
<pre>      # ccache speeds up Stan model compilation dramatically on warm cache.
      # Note: Windows support via ccache-action is documented as &quot;probably works&quot;
      # rather than fully stable; if it causes issues, scope this step to non-Windows.
      - name: Setup ccache
        uses: hendrikmuhs/ccache-action@v1.2.23
        with:
          # Key invalidates when Stan models or DESCRIPTION change.
          # Older caches partially seed new ones via restore-keys.
          key: ccache-${{ matrix.config.os }}-R-${{ matrix.config.r }}-${{ hashFiles('inst/stan/**/*.stan', 'DESCRIPTION') }}
          restore-keys: |
            ccache-${{ matrix.config.os }}-R-${{ matrix.config.r }}-
            ccache-${{ matrix.config.os }}-R-
          max-size: &quot;2G&quot;

      - name: Configure R to use ccache (Linux and macOS)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          mkdir -p ~/.R
          if [ &quot;$RUNNER_OS&quot; = &quot;macOS&quot; ]; then
            cat &gt;&gt; ~/.R/Makevars &lt;&lt;'EOF'
          CC = ccache clang
          CXX = ccache clang++
          CXX14 = ccache clang++
          CXX17 = ccache clang++
          CXX20 = ccache clang++
          EOF
          else
            cat &gt;&gt; ~/.R/Makevars &lt;&lt;'EOF'
          CC = ccache gcc
          CXX = ccache g++
          CXX14 = ccache g++
          CXX17 = ccache g++
          CXX20 = ccache g++
          EOF
          fi
          echo &quot;--- ~/.R/Makevars ---&quot;
          cat ~/.R/Makevars

      - name: Configure R to use ccache (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force -Path &quot;$HOME\.R&quot; | Out-Null
          $makevars = @&quot;
          CC = ccache gcc
          CXX = ccache g++
          CXX14 = ccache g++
          CXX17 = ccache g++
          CXX20 = ccache g++
          &quot;@
          Add-Content -Path &quot;$HOME\.R\Makevars.win&quot; -Value $makevars
          Write-Output &quot;--- ~/.R/Makevars.win ---&quot;
          Get-Content &quot;$HOME\.R\Makevars.win&quot;
</pre>
<p>You can see the 
<a href="https://github.com/okezie94/mrbayes/blob/master/.github/workflows/R-CMD-check.yaml" rel="nofollow" target="_blank">full file in my repo</a>.</p>
<p>This reduced my ubuntu-latest run for r-release from 7 minutes 30 seconds to 4 minutes 49 seconds.</p>
<h2 id="big-win-4-switch-to-clang">Big win 4: Switch to <code>clang</code></h2>
<p>I found that switching from <code>gcc</code> to <code>clang</code> gives a noticeable speedup; the single core compile time dropped from 3 minutes 55 seconds to 3 minutes flat on my Windows machine.</p>
<p>To do this you need to install <code>clang</code>. On Windows you install <code>clang</code> within RTools45 — more involved than on Linux, but doable.</p>
<pre># Windows within RTools45 Bash shell
# Launch C:\rtools45\ucrt64.exe
# You may need to close and reopen the shell after the first command
pacman -Syu
pacman -S mingw-w64-ucrt-x86_64-clang

# Ubuntu/Debian Linux
sudo apt install clang
</pre>
<p>At this point on Windows running</p>
<pre>which clang
</pre>
<p>should return <code>/ucrt/bin/clang</code>.</p>
<p>Switch to <code>clang</code> in <em>~/.R/Makevars</em> (if you’re not using <code>ccache</code> delete that prefix)</p>
<pre># On Linux
CC = ccache clang
CXX = ccache clang++
CXX14 = ccache clang++
CXX17 = ccache clang++
CXX20 = ccache clang++
</pre>
<p>and in <em>~/.R/Makevars.win</em> on Windows</p>
<pre># On Windows
CC = ccache C:/rtools45/ucrt64/bin/clang.exe
CXX = ccache C:/rtools45/ucrt64/bin/clang++.exe
CXX17 = ccache C:/rtools45/ucrt64/bin/clang++.exe
</pre>
<p>Windows users will need to add the following to <code>PATH</code></p>
<pre>C:\rtools45\ucrt64\bin
C:\rtools45\usr\bin
</pre>
<p>You can verify things are working by running</p>
<pre>R CMD config CXX17
</pre>
<p>I believe you need <code>clang</code> version 18 or later to see the speedups.</p>
<h2 id="small-win-1-wsl-users-should-use-the-native-file-system">Small win 1: WSL users should use the native file system</h2>
<p>Within WSL it is possible to access files from within its native Linux filesystem, i.e., within <code>/home/user/...</code>, and also on the Windows filesystem, e.g., in <code>/mnt/c/...</code>. I believe file operations are noticeably faster within <code>/home/user/...</code>.</p>
<h2 id="naive-guesses-that-made-no-difference">Naive guesses that made no difference</h2>
<p>I had wondered whether running a non-debug compilation with say</p>
<pre>pkgbuild::compile_dll(debug = FALSE)
</pre>
<p>would speed things up. It turns out it does not. For Stan models, most of the time is spent in C++ template instantiation by the compiler, not in optimisation passes — so disabling debug flags or lowering the optimisation level barely helps.</p>
<p>I also wondered whether using R on Windows Subsystem for Linux would speed things up just by virtue of being on Linux. It did not, timings using <code>gcc</code> on Windows and WSL Ubuntu were essentially identical. The advantage of using WSL is that it is easier to switch to using <code>clang</code> on Linux.</p>
<h2 id="money-no-object-big-win-5-switch-to-an-apple-silicon-mac">(Money no object) Big win 5: Switch to an Apple Silicon Mac</h2>
<p>Apple silicon Macs have excellent single threaded performance, their unified memory architecture has very high bandwidth, they have large L1 and L2 caches, and fast NVMe SSDs. Together these produce very fast Stan model compilation times, even on the lowest end Apple Silicon Macs.</p>
<h2 id="summary">Summary</h2>
<p>In summary, five big wins and one small win for speeding up Stan model compilation in R packages.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://remlapmot.github.io/post/2026/stan-compile-speedup/"> R | Dr Tom Palmer</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/speeding-up-stan-model-builds-for-r-package-developers/">Speeding up Stan model builds for R package developers</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401488</post-id>	</item>
		<item>
		<title>Behavior-Driven Development in R Shiny: Modeling User Behavior with When Steps</title>
		<link>https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-modeling-user-behavior-with-when-steps/</link>
		
		<dc:creator><![CDATA[jakub::sobolewski]]></dc:creator>
		<pubDate>Tue, 26 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://jakubsobolewski.com/blog/bdd-shiny-when</guid>

					<description><![CDATA[<p>Learn how to write When steps that describe user actions without leaking implementation details. Build a clean DSL that survives UI refactors and keeps specifications readable.</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-modeling-user-behavior-with-when-steps/">Behavior-Driven Development in R Shiny: Modeling User Behavior with When Steps</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-when"> jakub::sobolewski</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>A specification tells a story.</p>
<p>Given describes the world before the action. When describes the action itself. Then describes what changed as a result.</p>
<p>If Given is the setup and Then is the payoff, When is where the plot happens, and how you write it determines whether your specifications stay readable for months or decay after the very first change you make to your project.</p>
<hr>
<p>This article is part of a series on Behavior-Driven Development for Shiny applications. We’ve been building a data submission form from scratch, adding an email notification feature, and managing preconditions with Given steps.</p>
<p>Read the previous articles to get up to speed, or continue here to focus on how to write When steps.</p>
<ol>
<li><strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-feature/" rel="nofollow" target="_blank">Behavior-Driven Development in R Shiny: A Step-By-Step Example</a></strong></li>
<li><strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-given/" rel="nofollow" target="_blank">Behavior-Driven Development in R Shiny: Setting Up Test Preconditions with Given Steps</a></strong></li>
</ol>
<hr>
<blockquote>
<p><a href="https://jakubsobolewski.com/get-roadmap/" rel="nofollow" target="_blank">Level-up your testing game! Grab your copy of the R testing roadmap.</a></p>
</blockquote>
<h2 id="the-purpose-of-when">The Purpose of When</h2>
<p>When steps answer one question: <strong>What is the user doing?</strong></p>
<p>Not what button they clicked. Not what input ID was set. What <strong>they</strong> did, from <strong>their perspective</strong>.</p>
<p>The difference matters because UI implementations change constantly, user behaviors change only when business requirements change. Input IDs get renamed, buttons get moved, forms get refactored into modules. But users still interact with the same form, they submit the same data. If your When steps name those interface details, they break every time something shifts. If they describe user intent, they survive almost any UI change.</p>
<p>A When step should read like a sentence from a user story.</p>
<p>Compare these two:</p>
<pre># Implementation-coupled! Breaks when the submit button is removed or chaned
when_i_click_submit_button &lt;- function(context) {
  ...
}

# Behavior-focused! Survives any UI refactor
when_i_submit_entry_with_all_required_fields &lt;- function(context) {
  ...
}</pre>
<p>The second version tells you what the user accomplished, not how the app was wired.</p>
<p>That’s the goal.</p>
<h2 id="starting-the-app">Starting the App</h2>
<p>When steps are where the Shiny app launches — but not directly.</p>
<p>As we established in the Given article, the app starts lazily: the first When step calls <code>.start_driver()</code>, which creates the driver with all configuration accumulated from Given steps. Every subsequent When step interacts with the already-running app.</p>
<pre>#' tests/testthat/setup-dsl.R
when_i_submit_entry_with_all_required_fields &lt;- function(context) {
  .start_driver(context)
  context$driver$fill_required_fields(
    url = &quot;https://example.com/&quot;,
    title = &quot;Example&quot;
  )
  context
}</pre>
<p>Calling <code>.start_driver()</code> in every When step is safe — it’s a no-op if the driver already exists:</p>
<pre>.start_driver &lt;- function(context) {
  if (is.null(context$driver)) {
    context$driver &lt;- MyAppDriver$new(
      storage = context$storage,
      email_service = context$email_service,
      user = context$user
    )
  }
  context
}</pre>
<p>This means Given steps are purely configuration — they never touch the running app. The app only starts when the user’s first action is about to happen. This makes Given steps fast, composable, and easy to reason about.</p>
<h2 id="implementing-when-steps">Implementing When Steps</h2>
<p>The When step function itself should be thin. Its only job is to delegate to the driver and pass the context forward.</p>
<p>All implementation details live in the driver:</p>
<pre>#' tests/testthat/setup-driver.R
MyAppDriver &lt;- R6::R6Class(
  classname = &quot;MyAppDriver&quot;,
  inherit = shinytest2::AppDriver,
  public = list(
    fill_required_fields = function(url, title) {
      self$set_inputs(url = url, title = title)
      self$click(input = &quot;submit&quot;)
      self$wait_for_idle()
    },
    fill_all_fields = function(url, title, description) {
      self$set_inputs(url = url, title = title, description = description)
      self$click(input = &quot;submit&quot;)
      self$wait_for_idle()
    }
  )
)</pre>
<p>The driver encapsulates everything: which inputs to set, what to click, what to wait for. The When step just calls the right driver method by name.</p>
<p>This separation pays off immediately. If the submit button ID changes from <code>&quot;submit&quot;</code> to <code>&quot;btn_submit&quot;</code>, you fix it in one place — the driver method — and every specification that calls <code>fill_required_fields</code> continues to work unchanged.</p>
<p>Note that the same driver method can be called from multiple When steps.</p>
<p><strong>The driver is a translation layer.</strong> It translates user intent (fill required fields) into UI mechanics (set these inputs, click this button).</p>
<h2 id="naming-when-steps">Naming When Steps</h2>
<p>Good names follow a simple pattern: <code>when_&lt;who&gt;_&lt;verb&gt;_&lt;object&gt;</code>.</p>
<p>The verb describes the action from the user’s point of view. The object is what they acted on — expressed in domain language, not UI language.</p>
<pre># Good! We use domain language
when_i_submit_entry_with_all_required_fields()
when_i_submit_entry_with_missing_title()
when_i_delete_the_first_entry()
when_i_inspect_the_submissions_list()

# Bad! UI details are leaking in
when_i_click_the_submit_button()
when_i_clear_the_title_input()
when_i_navigate_to_slash_list()</pre>
<p>Domain names survive redesigns. UI names couple your specifications to the current implementation.</p>
<p>One more rule: <strong>one action per When step</strong>. A step named <code>when_i_fill_and_submit_the_form()</code> is doing two things. That makes it harder to compose and harder to understand which action caused which outcome.</p>
<pre># One action per step — composable
when_i_fill_in_the_form(url = &quot;https://example.com&quot;, title = &quot;Example&quot;) |&gt;
  when_i_submit_the_form()

# Doing too much — hard to reuse
when_i_fill_and_submit_the_form()</pre>
<p>Smaller, focused steps let you test multi-step flows precisely.</p>
<h3 id="my-thoughts-on-navigation-steps">My Thoughts on Navigation Steps</h3>
<p>You might think, this is an easy example.</p>
<p>Real apps aren’t single-page forms. They have navigation, multiple pages, modals, they might feel like navigating through a maze. It’s not that simple to interact with all the features! It’s not, but we can apply the same principles to more complex interactions.</p>
<p><strong>Here’s what I do.</strong></p>
<p>In apps we have pages. Users need to navigate to those pages before they can interact with them. How do we name When steps that represent navigation?</p>
<p>I prefer to name them based on their intent, what they’re trying to achieve, rather than the mechanics of how they get there. For example:</p>
<pre># Good! Focuses on the user's goal, not the UI structure
when_i_inspect_submissions()
when_i_open_the_first_entry()
when_i_change_settings(...) # composite step, implicitly navigates to the settings page and changes settings

# Bad! Reveals that there is a &quot;submissions&quot; page, what if we move submissions listing to another page?
when_i_navigate_to_submissions_page()</pre>
<p>This way, the step names reflect the user’s goals, not the UI structure. If we later change how navigation works — say we add another page and move our targeted component there — we can update the driver methods without changing the When step names.</p>
<p>That’s how specifications stay stable and focused on user behavior, not implementation details.</p>
<p>That’s how specifications stay stable and don’t need updating every time the UI changes.</p>
<h2 id="multi-step-flows">Multi-Step Flows</h2>
<p>When a scenario involves several user actions in sequence, chain them:</p>
<pre>it(&quot;should allow editing a submitted entry&quot;, {
  given_existing_entries() |&gt;
    given_an_authenticated_user() |&gt;
    when_i_open_the_first_entry() |&gt;
    when_i_update_the_title(title = &quot;Updated Title&quot;) |&gt;
    when_i_submit_the_form() |&gt;
    then_the_entry_has_title(&quot;Updated Title&quot;)
})</pre>
<p>Each step does one thing. The sequence reads like a user story. And if the assertion fails, you can see exactly which action preceded the broken state.</p>
<p>This is also where the context pipeline pattern earns its keep. Every step receives context and returns context, so you can add or remove steps in the middle without restructuring the whole test:</p>
<pre>it(&quot;should show confirmation after submitting&quot;, {
  given_no_content() |&gt;
    given_an_authenticated_user() |&gt;
    when_i_submit_entry_with_all_required_fields() |&gt;
    then_entry_is_submitted()
})</pre>
<p>The pipeline works because every step function — Given, When, or Then — has the same contract: receive <code>context</code>, return <code>context</code>. That uniformity is what lets pipe operator connect them.</p>
<p>Here is the pattern on its own, with no Shiny involved. The When steps are thin — each one just calls a driver method and returns context:</p>
<pre># A minimal driver that records what it was asked to do
MockDriver &lt;- function() {
  form &lt;- NULL
  submitted &lt;- FALSE
  list(
    fill_required_fields = function(url, title) {
      form &lt;&lt;- list(url = url, title = title)
    },
    submit = function() {
      submitted &lt;&lt;- TRUE
    },
    get_form = function() form,
    was_submitted = function() submitted
  )
}

# When steps are thin: call a driver method, return context
when_i_fill_in_the_form &lt;- function(context, url, title) {
  context$driver$fill_required_fields(url = url, title = title)
  context
}

when_i_submit_the_form &lt;- function(context) {
  context$driver$submit()
  context
}

ctx &lt;- new.env(parent = emptyenv())
ctx$driver &lt;- MockDriver()

ctx |&gt;
  when_i_fill_in_the_form(url = &quot;https://example.com&quot;, title = &quot;Example&quot;) |&gt;
  when_i_submit_the_form()</pre>
<p>The step functions contain no UI details — those live entirely inside <code>MockDriver</code>. Swapping the driver is all it takes to point the same steps at a different implementation.</p>
<h2 id="testing-error-paths">Testing Error Paths</h2>
<p>User interactions that trigger failures are still described with When steps. The step describes what the user attempted, not whether it succeeded.</p>
<pre>it(&quot;should require all required fields&quot;, {
  given_no_content() |&gt;
    given_an_authenticated_user() |&gt;
    when_i_submit_entry_with_missing_required_fields() |&gt;
    then_i_am_prompted_to_provide_required_fields()
})</pre>
<p>The When step looks identical to the happy-path version — which is the point. The user tried to submit with missing fields. The difference lives in the Then step, which checks what the app communicated back to the user.</p>
<pre>when_i_submit_entry_with_missing_required_fields &lt;- function(context) {
  .start_driver(context)
  context$driver$fill_required_fields(
    url = &quot;https://example.com/&quot;,
    title = &quot;&quot;  # Missing required field
  )
  context
}</pre>
<p>You don’t need to create special “error path” When steps with names like <code>when_i_incorrectly_submit()</code>. Name them after what the user did, not after whether it was correct. The Then step will check whether the app responded correctly to that action.</p>
<h2 id="parameterising-when-steps">Parameterising When Steps</h2>
<p>Sometimes you want to test multiple variations of the same action. Rather than creating a separate step for each variation, make the step accept parameters:</p>
<pre>when_i_submit_entry_with_title &lt;- function(context, title) {
  .start_driver(context)
  context$driver$fill_required_fields(
    url = &quot;https://example.com/&quot;,
    title = title
  )
  context
}</pre>
<p>Now you can express different scenarios clearly:</p>
<pre>it(&quot;should reject titles that are too short&quot;, {
  given_no_content() |&gt;
    given_an_authenticated_user() |&gt;
    when_i_submit_entry_with_title(title = &quot;ab&quot;) |&gt;
    then_i_am_prompted_to_provide_required_fields()
})

it(&quot;should accept titles of the minimum length&quot;, {
  given_no_content() |&gt;
    given_an_authenticated_user() |&gt;
    when_i_submit_entry_with_title(title = &quot;abc&quot;) |&gt;
    then_there_are_entries(n = 1)
})</pre>
<p>Use parameters when the difference between scenarios is data, not behavior. Keep the step name stable; let the arguments express the variation.</p>
<hr>
<h3 id="the-cucumber-way">The Cucumber Way</h3>
<p><a href="https://github.com/jakubsob/cucumber" rel="nofollow" target="_blank">cucumber</a> allows you to write multiple variants of scenarios with <code>Scenario Outline</code> and <code>Examples</code> keywords.</p>
<p>In a cucumber feature file this scenario would look like this:</p>
<pre>Feature: Data submission form validation
  Scenario Outline: Submitting entries with different title lengths
    Given no content
    And an authenticated user
    When I submit an entry with title &lt;title&gt;
    Then I am prompted to provide required fields
    Examples:
      | title |
      | &quot;ab&quot;  |
      | &quot;abc&quot; |</pre>
<p>In our implementation we don’t have a built-in way to express scenario outlines, but we can achieve the same effect by writing multiple scenarios that call the same parameterised When step.</p>
<hr>
<h2 id="signs-a-when-step-is-too-complex">Signs a When Step Is Too Complex</h2>
<p>A When step that runs more than a few lines of logic is usually trying to do too much.</p>
<ol>
<li><strong>If a step is setting state</strong> — checking whether something exists before interacting with it, or conditionally taking different actions — that logic belongs in Given steps or in the driver implementation, not the When step itself.</li>
<li><strong>If a step has branching logic</strong> — doing one thing when a flag is set and another when it isn’t — split it into two steps with clear names.</li>
<li><strong>If a step is making assertions</strong> — checking that something happened mid-flow — move those checks to Then steps. A When step should interact and nothing more. Mixing assertions into When steps makes failures ambiguous: you can’t tell whether the setup was wrong or the outcome was wrong. There might be exceptions to assert whether we’re on the right path in a multi-step flow – to fail eagerly – but generally, try to keep assertions in Then steps.</li>
</ol>
<h2 id="the-pattern-in-practice">The Pattern in Practice</h2>
<p>Here’s the full shape of a specification using the When steps we’ve built:</p>
<pre>describe(&quot;data submission&quot;, {
  it(&quot;should submit entry and send notification&quot;, {
    given_no_content() |&gt;
      given_an_authenticated_user() |&gt;
      given_email_service_is_available() |&gt;
      when_i_submit_entry_with_all_required_fields() |&gt;
      then_entry_is_submitted() |&gt;
      then_email_notification_is_sent()
  })

  it(&quot;should require all required fields&quot;, {
    given_no_content() |&gt;
      given_an_authenticated_user() |&gt;
      when_i_submit_entry_with_missing_required_fields() |&gt;
      then_i_am_prompted_to_provide_required_fields()
  })

  it(&quot;should allow editing a submitted entry&quot;, {
    given_existing_entries() |&gt;
      given_an_authenticated_user() |&gt;
      when_i_open_the_first_entry() |&gt;
      when_i_update_the_title(title = &quot;Updated Title&quot;) |&gt;
      when_i_submit_the_form() |&gt;
      then_the_entry_has_title(&quot;Updated Title&quot;)
  })
})</pre>
<p>The specifications read naturally. None of them mention input IDs, button selectors, or shinytest2. Those details live in the driver, hidden away from the scenarios they serve.</p>
<hr>
<p>Note how specifications don’t even reveal it’s a Shiny app. They don’t reveal how we interact with it — <code>shinytest2</code>, <code>selenider</code>, anything goes. It’s all hidden and irrelevant to the story we’re telling. You can swap the internals and keep the same specifications true. You can even swap the system implementation entirely, and as long as it still satisfies the same business requirements, the specifications don’t need touching. It’s live and executable — not a list of tickets in an external system no one reads.</p>
<p>That’s exactly the opposite of what happens when we write tests that are tightly coupled to the UI structure.</p>
<h2 id="wrapping-up">Wrapping up</h2>
<p>When steps model user behavior, not UI implementation.</p>
<p>That distinction is what keeps specifications readable days, weeks, months later, when the form has been refactored, button IDs have changed, and nobody remembers what the original implementation looked like. Keep steps focused on one action. Put the implementation details in the driver. Use domain language, not UI language.</p>
<p>Specifications written this way tend to survive refactors you haven’t thought of yet.</p>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://jakubsobolewski.com/blog/bdd-shiny-when"> jakub::sobolewski</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/behavior-driven-development-in-r-shiny-modeling-user-behavior-with-when-steps/">Behavior-Driven Development in R Shiny: Modeling User Behavior with When Steps</a>]]></content:encoded>
					
		
		<enclosure url="https://jakubsobolewski.com/rss-image.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401486</post-id>	</item>
		<item>
		<title>Repost: ctrlvee: Extract external R code and insert inline</title>
		<link>https://www.r-bloggers.com/2026/05/repost-ctrlvee-extract-external-r-code-and-insert-inline/</link>
		
		<dc:creator><![CDATA[Stephen Turner]]></dc:creator>
		<pubDate>Fri, 22 May 2026 13:34:48 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">http://www.r-bloggers.com/?guid=51c6de9f15cc0593fa890ee28b39bc84</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Reposted from the original at https://blog.stephenturner.us/p/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin. Ever find yourself looking through a pkgdown page or a Quarto<br />
book, copying and pasting code chunks from your brow...</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/repost-ctrlvee-extract-external-r-code-and-insert-inline/">Repost: ctrlvee: Extract external R code and insert inline</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="http://gettinggeneticsdone.blogspot.com/2026/05/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin.html"> Getting Genetics Done</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p><mark><b>Reposted from the original at <a href="https://blog.stephenturner.us/p/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin" rel="nofollow" target="_blank">https://blog.stephenturner.us/p/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin</a>.</b></mark> </p><p><span>Ever find yourself looking through a pkgdown page or a Quarto 
book, copying and pasting code chunks from your browser into your IDE? I
 do, and it’s a minor annoyance.</span><span data-state="closed" style="min-width: 0px;"><a class="footnote-anchor" data-component-name="FootnoteAnchorToDOM" href="https://blog.stephenturner.us/p/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin#footnote-1" id="footnote-anchor-1" rel="nofollow" target="_blank">1</a></span></p><p><span>My friend and colleague VP Nagraj published a new R package called </span><strong>ctrlvee</strong><span> that makes this a lot easier.</span></p><ul><li><p><strong><span>CRAN: </span><a href="https://cran.r-project.org/package=ctrlvee" rel="nofollow" target="_blank">https://cran.r-project.org/package=ctrlvee</a></strong></p></li><li><p><strong><span>GitHub: </span><a href="https://github.com/vpnagraj/ctrlvee" rel="nofollow" target="_blank">https://github.com/vpnagraj/ctrlvee</a></strong></p></li></ul><p><span>It
 does one thing. Put your cursor anywhere in an R script in Positron or 
RStudio, call the add-in, provide a URL, and a few milliseconds later 
you’ll have all the code from that page in your editor, separated by 
chunk boundaries (along with some metadata and a note to </span><a href="https://blog.stephenturner.us/p/pick-a-license-not-any-license" rel="nofollow" target="_blank">check the license!</a><span>).</span></p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img can-restack" data-component-name="Image2ToDOM" href="https://substackcdn.com/image/fetch/$s_!7WR6!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbfbd671-9318-421e-93b9-8ea4f5ef9e9a_1410x782.png" rel="nofollow" target="_blank"><div class="image2-inset"><picture><source type="image/webp"></source><img loading="lazy" alt="" class="sizing-large" data-attrs="{"src":"https://substack-post-media.s3.amazonaws.com/public/images/dbfbd671-9318-421e-93b9-8ea4f5ef9e9a_1410x782.png","srcNoWatermark":null,"fullscreen":false,"imageSize":"large","height":782,"width":1410,"resizeWidth":1200,"bytes":182436,"alt":null,"title":null,"type":"image/png","href":null,"belowTheFold":false,"topImage":true,"internalRedirect":"https://blog.stephenturner.us/i/197973095?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbfbd671-9318-421e-93b9-8ea4f5ef9e9a_1410x782.png","isProcessing":false,"align":"center","offset":false}" height="665 .531914893617" src="https://i1.wp.com/substackcdn.com/image/fetch/$s_!7WR6!,w_2400,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdbfbd671-9318-421e-93b9-8ea4f5ef9e9a_1410x782.png?resize=450%2C665&#038;ssl=1" width="450" data-recalc-dims="1" /></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"></div></div></div></a></figure></div><p><span>The package README provides a demonstration using the “Data Validation and QA” chapter of my </span><em>Data Science Team Training</em><span> book (</span><strong><a href="https://dstt.stephenturner.us/" rel="nofollow" target="_blank">dstt.stephenturner.us</a></strong><span>).</span></p><ol><li><p><span>Install the package: </span><code>install.packages(&quot;ctrlvee&quot;)</code></p></li><li><p><span>Run the add-in. In Positron you’ll open the command palette, search for Run RStudio Addin, then </span><em>extract external R code and insert inline</em><span>. You’ll get a modal asking you for a URL. </span></p></li><li><p><span>Paste one in. E.g., </span><strong>https://dstt.stephenturner.us/validation.html</strong></p></li><li><p>The R code from the website appears in your editor <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p></li></ol><p>Here’s a demo.</p><div class="videoScrollTarget-SzB20Y" data-component-name="VideoEmbedPlayer" id="media-32b2a9b2-d551-43a0-bffc-647ab75b032e"><div class="videoEmbed-_FycLU"><div aria-label="Video player" class="with-preview video-player-with-background video-player-wrapper" role="region"><div class="video-player video-player video-player-with-background videoPlayer-vlcedM" style="padding-bottom: 60%;"><video class="video-P2qgwZ" controls="" crossorigin="anonymous" data-video-id="32b2a9b2-d551-43a0-bffc-647ab75b032e" poster="https://substack-video.s3.amazonaws.com/video_upload/post/197973095/32b2a9b2-d551-43a0-bffc-647ab75b032e/transcoded-00001.png?refresh=Fri May 22 2026 09:32:43 GMT-0400 (Eastern Daylight Time)" preload="metadata"></video><div class="pencraft pc-position-absolute pc-reset buttonContainer-tH3LP9 video-player-button"></div></div></div></div></div><div class="subscribe-widget is-signed-up is-fully-subscribed" data-component-name="SubscribeWidget"><div class="pencraft pc-reset button-wrapper"><div class="pencraft pc-display-flex pc-justifyContent-center pc-reset"></div></div></div><p><span>Here’s what the extracted/inserted code looks like, from </span><a href="https://dstt.stephenturner.us/validation.html" rel="nofollow" target="_blank">this source</a><span>.</span></p><pre># -----------------------------------------------------------------
# Chunks fetched by ctrlvee from: https://dstt.stephenturner.us/validation.html
# Strategy: Rendered HTML page
# Date: 2026-05-16 05:14:44
# Chunks: 8
# NOTE: Check the source license before reusing this code.
# -----------------------------------------------------------------

flu &lt;- data.frame(
    week = c(1, 2, 3, 4, 4),
    county = c(&quot;Fairfax&quot;, &quot;Arlington&quot;, NA, &quot;Loudoun&quot;, &quot;Loudoun&quot;),
    disease = c(&quot;Flu&quot;, &quot;Flu&quot;, &quot;Flu&quot;, &quot;Flu&quot;, &quot;Flu&quot;),
    cases = c(23, 41, 18, -5, 12),
    rate = c(2.1, 3.8, 1.6, NA, 1.1)
)

flu

# ---- chunk boundary ----

if (any(flu$cases &lt; 0, na.rm = TRUE)) {
    stop(&quot;Negative case counts detected. Inspect raw data before proceeding.&quot;)
}

# ---- chunk boundary ----

stopifnot(
    &quot;Negative case counts&quot; = all(flu$cases &gt;= 0, na.rm = TRUE),
    &quot;Missing county values&quot; = !anyNA(flu$county),
    &quot;Duplicate records&quot; = !anyDuplicated(flu[, c(&quot;week&quot;, &quot;county&quot;)])
)

# ---- chunk boundary ----

install.packages(&quot;pointblank&quot;)

# ---- chunk boundary ----

library(pointblank)

agent &lt;- create_agent(tbl = flu, label = &quot;Weekly flu surveillance&quot;) |&gt;
    col_vals_gte(
        columns = cases,
        value = 0,
        label = &quot;Case counts must be non-negative&quot;
    ) |&gt;
    col_vals_not_null(
        columns = c(week, county),
        label = &quot;Week and county cannot be missing&quot;
    ) |&gt;
    rows_distinct(
        columns = c(week, county),
        label = &quot;No duplicate week/county records&quot;
    ) |&gt;
    interrogate()

agent

# ---- chunk boundary ----

create_agent(tbl = flu, label = &quot;Weekly flu surveillance — extended&quot;) |&gt;
    col_is_numeric(
        columns = c(cases, rate),
        label = &quot;Case count and rate must be numeric&quot;
    ) |&gt;
    col_vals_in_set(
        columns = disease,
        set = c(&quot;Flu&quot;, &quot;COVID-19&quot;, &quot;RSV&quot;),
        label = &quot;Disease must be from the approved list&quot;
    ) |&gt;
    col_vals_between(
        columns = week,
        left = 1,
        right = 52,
        label = &quot;Week must be between 1 and 52&quot;
    ) |&gt;
    col_vals_gte(
        columns = rate,
        value = 0,
        na_pass = TRUE,
        label = &quot;Rate must be non-negative (NAs allowed)&quot;
    ) |&gt;
    interrogate()

# ---- chunk boundary ----

if (!all_passed(agent)) {
    stop(&quot;Data validation failed. Review the agent report before proceeding.&quot;)
}

# ---- chunk boundary ----

library(readr)
library(pointblank)

flu &lt;- read_csv(&quot;data/flu-2024.csv&quot;)

# Validate immediately after reading
agent &lt;- create_agent(tbl = flu, label = &quot;flu-2024 validation&quot;) |&gt;
    col_vals_gte(columns = cases, value = 0, label = &quot;No negative counts&quot;) |&gt;
    col_vals_not_null(columns = c(week, county), label = &quot;No missing keys&quot;) |&gt;
    rows_distinct(columns = c(week, county), label = &quot;No duplicate records&quot;) |&gt;
    interrogate()

if (!all_passed(agent)) {
    stop(&quot;Validation failed — see agent report above.&quot;)
}</pre><div class="subscribe-widget is-signed-up is-fully-subscribed" data-component-name="SubscribeWidget"><div class="pencraft pc-reset button-wrapper"><div class="pencraft pc-display-flex pc-justifyContent-center pc-reset"></div></div></div><div class="footnote" data-component-name="FootnoteToDOM"><br /></div><p> </p><div class="blogger-post-footer">Getting Genetics Done by Stephen Turner is licensed under a Creative Commons Attribution (CC BY) License.</div>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="http://gettinggeneticsdone.blogspot.com/2026/05/ctrlvee-extract-external-r-code-insert-inline-positron-rstudio-addin.html"> Getting Genetics Done</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/repost-ctrlvee-extract-external-r-code-and-insert-inline/">Repost: ctrlvee: Extract external R code and insert inline</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401398</post-id>	</item>
		<item>
		<title>[R] How to use ggpattern to add patterns to ggplot2 plots</title>
		<link>https://www.r-bloggers.com/2026/05/r-how-to-use-ggpattern-to-add-patterns-to-ggplot2-plots/</link>
		
		<dc:creator><![CDATA[R on Zhenguo Zhang&#039;s Blog]]></dc:creator>
		<pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Zhenguo Zhang's Blog https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/ -</p>
<p>Adding patterns to plots is a great way to improve accessibility (making plots colorblind-friendly) and to add an extra dimension of information. The ggpattern pac...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/r-how-to-use-ggpattern-to-add-patterns-to-ggplot2-plots/">[R] How to use ggpattern to add patterns to ggplot2 plots</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/"> R on Zhenguo Zhang&#039;s Blog</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
Zhenguo Zhang&#8217;s Blog https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/ &#8211;


<p>Adding patterns to plots is a great way to improve accessibility (making plots colorblind-friendly) and to add an extra dimension of information. The <code>ggpattern</code> package provides a rich set of tools to achieve this in <code>ggplot2</code>.</p>
<div id="basic-example-with-mtcars" class="section level2">
<h2>Basic Example with mtcars</h2>
<p>Let’s start with a barplot using the <code>mtcars</code> dataset. We will map <code>cyl</code> to the x-axis, <code>mpg</code> to the y-axis, use <code>gear</code> for the fill color, and <code>am</code> for the pattern.</p>
<pre># Prepare data
df &lt;- mtcars %&gt;%
  mutate(
    cyl = factor(cyl),
    gear = factor(gear),
    am = factor(am, labels = c(&quot;Automatic&quot;, &quot;Manual&quot;))
  )

# Create barplot
ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    color = &quot;black&quot;,
    pattern_fill = &quot;black&quot;,
    pattern_spacing = 0.02,
    pattern_key_scale_factor = 1.5
  ) +
  scale_pattern_manual(values = c(&quot;stripe&quot;, &quot;crosshatch&quot;)) +
  labs(
    title = &quot;MPG by Cylinders, Gear, and Transmission&quot;,
    subtitle = &quot;Patterns representing transmission type&quot;,
    x = &quot;Cylinders&quot;,
    y = &quot;Miles Per Gallon (MPG)&quot;,
    fill = &quot;Gears&quot;,
    pattern = &quot;Transmission&quot;
  ) +
  theme_minimal()</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/basic-example-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="exploring-key-parameters" class="section level2">
<h2>Exploring Key Parameters</h2>
<p>The <code>ggpattern</code> package allows for fine-grained control over how patterns are rendered. Here are some of the most important parameters:</p>
<div id="pattern_key_scale_factor" class="section level3">
<h3>1. pattern_key_scale_factor</h3>
<p>This parameter scales the pattern density specifically in the legend. Without it, patterns in the legend can often be hard to see.
Reduce the value will make it easier fit into the legend keys.</p>
<pre>p_base &lt;- ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    color = &quot;black&quot;,
    pattern_spacing = 0.02
  ) +
  theme_minimal() +
  labs(title = &quot;Default vs Scaled Legend&quot;)

# Default legend
p1 &lt;- p_base + labs(subtitle = &quot;Default (key_scale_factor = 1)&quot;)

# Scaled legend
p2 &lt;- p_base + 
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    color = &quot;black&quot;,
    pattern_spacing = 0.02,
    pattern_key_scale_factor = 0.5
  ) +
  labs(subtitle = &quot;Scaled (key_scale_factor = 0.5)&quot;)

p1</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/scale-factor-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
<pre>p2</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/scale-factor-2.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="pattern_spacing-and-pattern_density" class="section level3">
<h3>2. pattern_spacing and pattern_density</h3>
<p><code>pattern_spacing</code> controls the distance between pattern elements (lower = denser), while <code>pattern_density</code> controls the coverage (higher = denser).</p>
<pre># Spacing effect
ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    pattern_spacing = 0.2, # Wider spacing
    pattern_key_scale_factor = 1.5
  ) +
  labs(title = &quot;Effect of pattern_spacing = 0.5 (Sparser)&quot;) +
  theme_minimal()</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/spacing-density-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
<pre># Density effect
ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    pattern_density = 0.8, # Higher density
    pattern_key_scale_factor = 1.5
  ) +
  labs(title = &quot;Effect of pattern_density = 0.8 (Denser)&quot;) +
  theme_minimal()</pre>
<p><img src="https://i2.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/spacing-density-2.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="pattern_fill" class="section level3">
<h3>3. pattern_fill</h3>
<p><code>pattern_fill</code> sets the color of the pattern elements themselves.</p>
<pre>ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern = am),
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    pattern_fill = &quot;white&quot;, # White pattern lines
    pattern_density = 0.5, # make it bigger so that the color is easier to see
    pattern_spacing = 0.02,
    pattern_key_scale_factor = 1.5
  ) +
  labs(title = &quot;Effect of pattern_fill = &#39;white&#39;&quot;) +
  theme_minimal()</pre>
<p><img src="https://i0.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/pattern-fill-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
<div id="pattern_shape" class="section level3">
<h3>4. pattern_shape</h3>
<p>For geometric patterns like <code>regular_polygon</code>, <code>pattern_shape</code> controls the number of sides.</p>
<pre>ggplot(df, aes(x = cyl, y = mpg, fill = gear)) +
  geom_bar_pattern(
    aes(pattern_shape=gear),
    pattern = &quot;regular_polygon&quot;,
    stat = &quot;identity&quot;,
    position = &quot;dodge&quot;,
    pattern_size = 0.5, # stroke line width
    pattern_density = 0.5, # increase the shape size
    pattern_spacing = 0.03,
    pattern_key_scale_factor = 1.5
  ) +
  scale_pattern_shape_manual(values = c(&quot;3&quot;=&quot;circle&quot;, &quot;4&quot;=&quot;convex6&quot;, &quot;5&quot;=&quot;square&quot;)) + # manually set shapes
  labs(title = &quot;Effect of mapping pattern_shape to gear&quot;) +
  theme_minimal()</pre>
<p><img src="https://i2.wp.com/fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/index_files/figure-html/pattern-shape-1.png?w=450&#038;ssl=1" alt="" data-recalc-dims="1" /></p>
</div>
</div>
<div id="troubleshooting" class="section level2">
<h2>Troubleshooting</h2>
<p>If you encounter issues installing or loading <code>ggpattern</code> on Linux, you may need to install the following system dependencies:</p>
<pre>sudo apt-get install libudunits2-dev
sudo apt install libproj22
sudo apt-get install libgdal-dev</pre>
</div>
- https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/ - 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://fortune9.netlify.app/2026/05/22/r-how-to-use-ggpattern/"> R on Zhenguo Zhang&#039;s Blog</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/r-how-to-use-ggpattern-to-add-patterns-to-ggplot2-plots/">[R] How to use ggpattern to add patterns to ggplot2 plots</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401411</post-id>	</item>
		<item>
		<title>Functions over Idioms &#8211; Writing R in Python with rfuns</title>
		<link>https://www.r-bloggers.com/2026/05/functions-over-idioms-writing-r-in-python-with-rfuns/</link>
		
		<dc:creator><![CDATA[Jonathan Carroll]]></dc:creator>
		<pubDate>Fri, 22 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://jcarroll.com.au/2026/05/22/functions-over-idioms-rfuns/</guid>

					<description><![CDATA[<p>If you’ve read any of my past posts you know I like to program in several<br />
different languages, some of which I like more than others. Sometimes a problem<br />
calls for a particular language to be used, and with that comes adjusting one’s<br />
brain to thinking in that ...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/functions-over-idioms-writing-r-in-python-with-rfuns/">Functions over Idioms – Writing R in Python with rfuns</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://jcarroll.com.au/2026/05/22/functions-over-idioms-rfuns/"> rstats on Irregularly Scheduled Programming</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>If you’ve read any of my past posts you know I like to program in several
different languages, some of which I like more than others. Sometimes a problem
calls for a particular language to be used, and with that comes adjusting one’s
brain to thinking in that language and using the appropriate idioms to leverage
that language’s features. But what if I don’t want to?</p>
<div class="float">
<img src="https://i2.wp.com/jcarroll.com.au/2026/05/22/functions-over-idioms-rfuns/images/IDontWantTo.gif?w=450&#038;ssl=1" alt="I don’t want to" data-recalc-dims="1" />
<div class="figcaption">I don’t want to</div>
</div>
<p>The line between R and Python has been heavily blurred the last few years,
particularly with <a href="https://rstudio.github.io/reticulate/" rel="nofollow" target="_blank">{reticulate}</a> enabling
us to use Python within R code, RStudio rebranding as <a href="https://posit.co/" rel="nofollow" target="_blank">Posit</a>
and taking on a strong Python development effort, releasing
<a href="https://posit.co/products/ide/positron" rel="nofollow" target="_blank">Positron</a> as a multi-language IDE, and
<a href="https://quarto.org/" rel="nofollow" target="_blank">Quarto</a> being a multi-language rethink of Rmarkdown.</p>
<p>I occasionally <em>need</em> to use Python directly &#8211; an SDK wrapping an API exists and
I don’t particularly want to spend a lot of time writing my own R version,
especially before I know what I want to get out of the endpoints. At this point
I tend to bump up against my muscle-memory from R and try to use functions I’m
familiar with from R, but which don’t actually exist in Python. Now, that might
sometimes be because the pattern I’m trying to encode simply has a different name
in Python; instead of an <code>sapply(x, f)</code></p>
<pre>sapply(c(2, 3, 4, 5), \(x) x ^ 2)
## [1]  4  9 16 25</pre>
<p>I should reach for <code>map</code>, in which case I am reminded that this produces a lazy
iterator that doesn’t show me the results</p>
<pre>map(lambda x: x ** 2, [2, 3, 4, 5])
## &lt;map object at 0x10d7fbee0&gt;</pre>
<p>and so I need to wrap it into a list to get the values out</p>
<pre>list(map(lambda x: x ** 2, [2, 3, 4, 5]))
## [4, 9, 16, 25]</pre>
<p>Or, I could use a list comprehension which <em>isn’t</em> lazy</p>
<pre>[v ** 2 for v in [2, 3, 4, 5]]
## [4, 9, 16, 25]</pre>
<p>That’s the <em>idiom</em> that I should be reaching for. Sure.</p>
<p>Other times there’s a package I need to use and a slightly different way of
approaching the problem. In R I love the <code>table()</code> function for getting
histogram-like counts of the unique values of a vector</p>
<pre>table(c(&quot;b&quot;, &quot;a&quot;, &quot;c&quot;, &quot;a&quot;, &quot;b&quot;, &quot;a&quot;))
## 
## a b c 
## 3 2 1</pre>
<p>which in Python looks like</p>
<pre>from collections import Counter

sorted(Counter([&quot;b&quot;, &quot;a&quot;, &quot;c&quot;, &quot;a&quot;, &quot;b&quot;, &quot;a&quot;]).items())
## [(&#39;a&#39;, 3), (&#39;b&#39;, 2), (&#39;c&#39;, 1)]</pre>
<p>Probably Pythonistas remember that idiom and the package to import and the
<code>.items()</code> extractor and the fact that they maybe want to sort the result. But I
kept coming back to a question I ask myself: <em>what if I don’t want to</em>? Why is
there not a function that wraps this idiom? If there was, why not just call it
“table”? Admittedly, it’s far from the catchiest, most memorable, or most useful
name, but it’s immediately recognisable to an R user (ditto for “sapply”).</p>
<p>One approach I considered here was to just call R from Python. That <em>can</em> be done,
but I doubt I or anyone else wants to deal with that every time we want to iterate
over a list. There’s a package on the Python package index which seems to support
this nicely: <a href="https://pypi.org/project/r-functions/" class="uri" rel="nofollow" target="_blank">https://pypi.org/project/r-functions/</a> but it’s wrappers around
individual R files, via RScript. I’m thinking more along the lines of ‘native
Python with an R interface’.</p>
<p>Python is an object-oriented language, but it <em>has</em> functions, so why not make one</p>
<pre>from collections import Counter

def table(x):
    return dict(sorted(Counter(x).items()))

table([&quot;b&quot;, &quot;a&quot;, &quot;c&quot;, &quot;a&quot;, &quot;b&quot;, &quot;a&quot;])
## {&#39;a&#39;: 3, &#39;b&#39;: 2, &#39;c&#39;: 1}
def sapply(x, func):
    return [func(v) for v in x]
  
sapply([2, 3, 4, 5], lambda x: x ** 2)
## [4, 9, 16, 25]</pre>
<p>and have a nicer function interface to apply these idioms? I thought about this
a bit longer, and realised there’s <strong>lots</strong> of functions I use in R that I wish
I could use in Python. An idiom for finding the index of elements of a ‘vector’
(list in Python) which are true (<code>TRUE</code> in R, <code>True</code> in Python) is</p>
<pre>[i for i, v in enumerate(x) if v]</pre>
<p>but I just want to call <code>which(x)</code></p>
<pre>which(c(FALSE, FALSE, TRUE, FALSE , TRUE))
## [1] 3 5</pre>
<p>so why not define this</p>
<pre>def which(x):
    return [i for i, v in enumerate(x) if v]
  
which([False, False, True, False, True])
## [2, 4]</pre>
<p>(remembering that Python is 0-indexed).</p>
<p>How far could one take this? Quite a long way!</p>
<p>I thought more about what differences would need to be accounted for, and one that
immediately came to mind was that R is vectorised. If I was to recreate R’s
character counting function <code>nchar(s)</code> as essentially <code>len(s)</code>, I’d need to consider
whether I wanted it to work on a single string or a ‘vector’ of strings</p>
<p>In R:</p>
<pre>nchar(c(&quot;these&quot;, &quot;all&quot;, &quot;have&quot;, &quot;different&quot;, &quot;lengths&quot;))
## [1] 5 3 4 9 7</pre>
<p>But in Python, <code>len()</code> expects a single value, so it calculates the length of
the list</p>
<pre>len([&quot;these&quot;, &quot;all&quot;, &quot;have&quot;, &quot;different&quot;, &quot;lengths&quot;])
## 5</pre>
<p>The ‘proper’ way to do it is to map over the list</p>
<pre>[len(s) for s in [&quot;these&quot;, &quot;all&quot;, &quot;have&quot;, &quot;different&quot;, &quot;lengths&quot;]]
## [5, 3, 4, 9, 7]</pre>
<p>but again, why do I need to use an idiom for this? What if I just made a decorator
to change a regular function to a vectorised one by applying this list
comprehension internally when it’s passed a list (or a tuple), and which otherwise
just evaluates the function with the argument?</p>
<pre>import functools

def make_vec(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if isinstance(args[0], (list, tuple)):
            return [func(xi, *args[1:], **kwargs) for xi in args[0]]
        return func(*args, **kwargs)
    return wrapper

@make_vec
def my_len(s):
    return len(s)

my_len([&quot;these&quot;, &quot;all&quot;, &quot;have&quot;, &quot;different&quot;, &quot;lengths&quot;])
## [5, 3, 4, 9, 7]</pre>
<p>and I could name it… “nchar”!</p>
<p>The other use-case that came to mind was <a href="https://fosstodon.org/deck/@eliocamp@mastodon.social/116531644286276585" rel="nofollow" target="_blank">Elio venting</a>
(and referencing a post to which <a href="https://jcarroll.com.au/2025/12/05/haskell-is-a-great-language-for-data-science/" rel="nofollow" target="_blank">I also wrote a sort of response</a>)
that they needed to list the files in the current directory</p>
<blockquote class="mastodon-embed" data-embed-url="https://mastodon.social/@eliocamp/116531644254157709/embed" style="background: #FCF8FF; border-radius: 8px; border: 1px solid #C9C4DA; margin: 0; max-width: 540px; min-width: 270px; overflow: hidden; padding: 0;">
<a href="https://mastodon.social/@eliocamp/116531644254157709" style="align-items: center; color: #1C1A25; display: flex; flex-direction: column; font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', Roboto, sans-serif; font-size: 14px; justify-content: center; letter-spacing: 0.25px; line-height: 20px; padding: 24px; text-decoration: none;" rel="nofollow" target="_blank"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="32" height="32" viewBox="0 0 79 75"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"/></svg>
<div style="color: #787588; margin-top: 16px;">
Post by <span class="citation">@eliocamp</span><span class="citation">@mastodon.social</span>
</div>
<div style="font-weight: 500;">
View on Mastodon
</div>
</a>
</blockquote>
<script data-allowed-prefixes="https://mastodon.social/" async src="https://mastodon.social/embed.js"></script>
<p>with the idiom</p>
<pre>import os

[os.path.join(path, f) for f in os.listdir(path)]</pre>
<p>The supplied suggestions included</p>
<pre>from pathlib import Path

list(Path(path).iterdir())</pre>
<p>(just rolls off the tongue, doesn’t it?) which returns a list of <code>PosixPath()</code>
objects and is hardly easy to parse visually.</p>
<p>So, why not have a function?!?</p>
<pre>import os

def list_files(path):
    return [os.path.join(path, f) for f in os.listdir(path)]

path = &quot;path/to/files&quot;

list_files(path)
## [&#39;path/to/files/file1.txt&#39;, &#39;path/to/files/file2.txt&#39;, &#39;path/to/files/file3.csv&#39;]</pre>
<p>I would have liked to call this <code>list.files()</code> but, since Python strictly uses
the dot for method calling, it can’t be that.</p>
<p>This then raises the question of “should I support the arguments already in the R
functions?” In this case, should it support a <code>recursive</code> argument? Yes, that
adds complexity, but it’s surely do-able. At this point I reached for some AI
assistance and had Claude help me to implement as many functions as we could think
of, supporting as many common arguments as possible. This involved extending the
decorator to support vectorising other arguments (which also need to be careful
about dots).</p>
<p>On testing it out, it looked like we had something viable.</p>
<p>One last piece I wanted to support, though: the <code>which()</code> example above extracts
the elements of a <em>logical</em> vector which are <code>True</code>, but in order to build that vector
in the first place, I would naturally leverage R’s vectorisation as an array
language. The two steps involved here are to first compute the comparison resulting
in a logical vector, then to use <code>which()</code> to identify the indices of those which are
true</p>
<pre>which(c(&quot;c&quot;, &quot;b&quot;, &quot;a&quot;, &quot;c&quot;, &quot;a&quot;, &quot;b&quot;) == &quot;a&quot;)
## [1] 3 5</pre>
<p>The vectorisation decorator above doesn’t help here, because it’s at the point of
<code>==</code> that we want to vectorise</p>
<pre>[&#39;c&#39;, &#39;b&#39;, &#39;a&#39;, &#39;c&#39;, &#39;a&#39;, &#39;b&#39;] == &#39;a&#39;
## False</pre>
<p>This is <code>False</code> because the character <code>'a'</code> is not equal to the given list.</p>
<p>The appropriate idiom is once again to use a list comprehension</p>
<pre>which(x == &#39;a&#39; for x in [&#39;c&#39;, &#39;b&#39;, &#39;a&#39;, &#39;c&#39;, &#39;a&#39;, &#39;b&#39;])
## [2, 4]</pre>
<p>The solution I’m fond of is to create a new ‘Vec’ class which wraps binary operators
with a list comprehension, again abstracting away this detail. This means
implementing <code>__eq__</code>, <code>__add__</code>, <code>__and__</code> and lots of other binary operations,
but with that, and a wrapper to create such an object, the comparison operators
can be vectorised</p>
<pre>vals = vec([&#39;c&#39;, &#39;b&#39;, &#39;a&#39;, &#39;c&#39;, &#39;a&#39;, &#39;b&#39;])
which(vals == &#39;a&#39;)
## [2, 4]</pre>
<p>Not pristine, but quite clean, if you ask me.</p>
<p>With all these pieces in place, adding implementations for common base R functions
including most arguments and a way to vectorise lists, I wrapped everything up
into a Python package (my first) to learn how to do it.</p>
<p>The workflow isn’t particularly painful, with my biggest complication being
different versions of Python supporting different requirements in <code>pyproject.toml</code>,
and so some GitHub Actions are failing because of that.</p>
<p>As part of building out the implementations I had Claude add tests for each of the
functions with some expected values &#8211; if I <em>do</em> want to improve some of the idioms
internally, I want to ensure I don’t change the values produced. That works for
having any testing at all, but how can I be sure that I’m reproducing what I
would get if I was working in R? One option was to just run all of the test
functions by hand and confirm that the values look similar enough, accounting for
list vs vector and 0 vs 1 indexing. Instead, Claude managed to write an adaptor
for <code>pytest</code> which does the realignment of e.g. <code>list_files</code> to <code>list.files</code>
(and similarly for arguments), realigns the indexing where needed, and runs all
existing tests directly in R via <code>rpy2</code> (skipping over some for which I don’t
have tests yet). I’m disabling automated testing of this because I suspect it
could get flaky dealing with both R <em>and</em> Python on GitHub Actions, but I can
confirm that all the current tests pass.</p>
<p>I wanted to have a documentation website similar to what we have via {pkgdown} and
came across <a href="https://github.com/machow/quartodoc" rel="nofollow" target="_blank">quartodoc</a> which is what the
<a href="https://rstudio.github.io/pins-python/" rel="nofollow" target="_blank">Python version of {pins} uses</a>. Getting
that to work required downgrading a specific Python dependency, but was otherwise
painless.</p>
<p>I have a working package locally &#8211; how do I share it? This seemed like the perfect
opportunity to learn what the release process looks like for Python. I have a
handful of packages on CRAN and one on Bioconductor, and the process there is
far from frictionless, with the side-effect that there’s some trust you can place
on the interoperability of packages and minimal (automated) code checking. While
Python is more ‘wild west’ in terms of what can be uploaded, it’s really nice to see
that they do have an <a href="https://test.pypi.org/" rel="nofollow" target="_blank">entirely separate test server</a>
where you can upload your package and see how it looks. I’m reminded of the quote</p>
<blockquote>
<p>Everybody has a testing environment. Some people are lucky enough to have a totally separate environment to run production in.</p>
</blockquote>
<p>Given that it’s not currently possible to run 100% of the CRAN checks locally
(and even some that you <em>can</em> give a different result to what’s on their systems)
this does make me a little jealous. I wonder whether the decrease in load from
rejecting failing submissions would offset supporting a test submission server.</p>
<p>All went well pushing to the test server (via an authentication key) and I managed
to build up the courage to push to the production instance…
<a href="https://pypi.org/project/rfuns/" rel="nofollow" target="_blank">it’s live!</a></p>
<div class="float">
<img src="https://i2.wp.com/jcarroll.com.au/2026/05/22/functions-over-idioms-rfuns/images/rfuns_logo_small.png?w=300&#038;ssl=1" alt="rfuns logo - R functions in Python… are fun" data-recalc-dims="1" />
<div class="figcaption">rfuns logo &#8211; R functions in Python… are fun</div>
</div>
<p>and the <a href="https://jonocarroll.github.io/rfuns/" rel="nofollow" target="_blank">documentation site</a> isn’t too bad,
either (in my opinion).</p>
<p>This means that you can now run</p>
<pre>uv add rfuns</pre>
<p>(or the equivalent in whatever virtual environment management configuration you’re
using, e.g. <code>pip install rfuns</code>) and start using some R functions directly in
Python!</p>
<p>Depending on how you like to manage your imports, you can import everything</p>
<pre>from rfuns import *

which([False, False, True, False, True])
## [2, 4]</pre>
<p>or, if you prefer to namespace</p>
<pre>import rfuns as r

r.which([False, False, True, False, True])
## [2, 4]</pre>
<p>The list of functions currently imported, grouped into sections is:</p>
<div id="strings" class="section level3">
<h3>Strings</h3>
<ul>
<li><code>nchar(x)</code></li>
<li><code>nzchar(x)</code></li>
<li><code>paste(*args, sep=&quot; &quot;, collapse=None)</code></li>
<li><code>paste0(*args, collapse=None)</code></li>
<li><code>grepl(pattern, x, ignore_case=False, fixed=False)</code></li>
<li><code>grep(pattern, x, ignore_case=False, fixed=False, value=False, invert=False)</code></li>
<li><code>gsub(pattern, replacement, x, ignore_case=False, fixed=False)</code></li>
<li><code>sub(pattern, replacement, x, ignore_case=False, fixed=False)</code></li>
<li><code>trimws(x, which=&quot;both&quot;, whitespace=r&quot;[ \t\r\n]&quot;)</code></li>
<li><code>toupper(x)</code></li>
<li><code>tolower(x)</code></li>
<li><code>startsWith(x, prefix)</code></li>
<li><code>endsWith(x, suffix)</code></li>
<li><code>strsplit(x, split, fixed=False)</code></li>
<li><code>substr(x, start, stop)</code></li>
<li><code>chartr(old, new, x)</code></li>
<li><code>formatC(x, digits=6, format=&quot;g&quot;, width=None)</code></li>
</ul>
</div>
<div id="vectors" class="section level3">
<h3>Vectors</h3>
<ul>
<li><code>which(x)</code></li>
<li><code>which_min(x)</code></li>
<li><code>which_max(x)</code></li>
<li><code>diff(x, lag=1)</code></li>
<li><code>cumsum(x)</code></li>
<li><code>cumprod(x)</code></li>
<li><code>cummax(x)</code></li>
<li><code>cummin(x)</code></li>
<li><code>rev(x)</code></li>
<li><code>duplicated(x)</code></li>
<li><code>setdiff(x, y)</code></li>
<li><code>intersect(x, y)</code></li>
<li><code>union(x, y)</code></li>
<li><code>unique(x)</code></li>
<li><code>seq_along(x)</code></li>
<li><code>seq_len(n)</code></li>
<li><code>seq(from_=0, to=None, by=None, length_out=None)</code> (<code>from</code> is a reserved keyword)</li>
<li><code>sign(x)</code></li>
<li><code>r_range(x)</code> (renamed to not conflict with <code>range()</code>)</li>
</ul>
</div>
<div id="math" class="section level3">
<h3>Math</h3>
<ul>
<li><code>sign(x)</code></li>
<li><code>trunc(x)</code></li>
<li><code>ceiling(x)</code></li>
<li><code>floor(x)</code></li>
<li><code>sqrt(x)</code></li>
<li><code>log(x, base=None)</code></li>
<li><code>log2(x)</code></li>
<li><code>log10(x)</code></li>
<li><code>exp(x)</code></li>
<li><code>abs(x)</code></li>
<li><code>var(x, na_rm=False)</code></li>
<li><code>sd(x, na_rm=False)</code></li>
<li><code>mean(x, na_rm=False)</code></li>
<li><code>median(x, na_rm=False)</code></li>
<li><code>quantile(x, probs=None, na_rm=False)</code></li>
<li><code>scale(x, center=True, scale_=True)</code></li>
<li><code>round(x, digits=0)</code></li>
</ul>
</div>
<div id="files" class="section level3">
<h3>Files</h3>
<ul>
<li><code>list_files(path=&quot;.&quot;, pattern=None, all_files=False, full_names=False, recursive=False, ignore_case=False, include_dirs=False, no_dot=False)</code></li>
<li><code>file_exists(path)</code></li>
<li><code>dir_exists(path)</code></li>
<li><code>basename(path)</code></li>
<li><code>dirname(path)</code></li>
<li><code>file_path(*args)</code></li>
</ul>
</div>
<div id="table" class="section level3">
<h3>Table</h3>
<ul>
<li><code>table(x)</code></li>
<li><code>prop_table(x)</code></li>
<li><code>margin_table(x)</code></li>
</ul>
</div>
<div id="functional" class="section level3">
<h3>Functional</h3>
<ul>
<li><code>lapply(x, func)</code></li>
<li><code>sapply(x, func)</code></li>
<li><code>vapply(x, func, expected_type)</code></li>
<li><code>tapply(x, index, func)</code></li>
<li><code>rapply(x, func)</code></li>
<li><code>Filter(func, x)</code></li>
<li><code>Map(func, *args)</code></li>
<li><code>Reduce(func, x, init=None, accumulate=False)</code></li>
</ul>
</div>
<div id="inspect" class="section level3">
<h3>Inspect</h3>
<ul>
<li><code>head(x, n=6)</code></li>
<li><code>tail(x, n=6)</code></li>
<li><code>length(x)</code></li>
<li><code>nrow(x)</code></li>
<li><code>ncol(x)</code></li>
<li><code>dim(x)</code></li>
<li><code>summary(x)</code></li>
<li><code>rstr(x)</code> (renamed to not conflict with <code>str()</code>)</li>
</ul>
</div>
<div id="utils" class="section level3">
<h3>Utils</h3>
<ul>
<li><code>vec(x)</code></li>
</ul>
<p>Some of these are vectorised</p>
<pre>nchar([&quot;these&quot;, &quot;all&quot;, &quot;have&quot;, &quot;different&quot;, &quot;lengths&quot;])
## [5, 3, 4, 9, 7]
grepl(&quot;ar&quot;, [&quot;frog&quot;, &quot;carpet&quot;, &quot;basket&quot;, &quot;dart&quot;])
## [False, True, False, True]
sqrt([36, 81, 9])
## [6.0, 9.0, 3.0]</pre>
<p>while others (approximately, up to 0-indexing) preserve the R behaviour, such as
how <code>seq()</code> works</p>
<pre>seq(5)
## [0, 1, 2, 3, 4]
seq(from_=0, to=10, by=2)
## [0, 2, 4, 6, 8, 10]</pre>
<p>(note that <code>from</code> is a keyword in Python, so the argument here is now <code>from_</code>)
and set operations</p>
<pre>setdiff([5, 2, 4, 1], [2, 1])
## [5, 4]</pre>
<p>whereas this does not preserve order</p>
<pre>set([5, 2, 4, 1]) -  set([2, 1])
## {4, 5}</pre>
<p>Doing <em>all</em> of this myself would have taken quite some time, so I’m grateful to
be able to direct an agent towards accomplishing some of the tedious parts of this
project. I still drove the decision making and made sure to verify outputs, so I
don’t consider this a ‘vibe-coded’ project.</p>
<p>I’m not recommending you use this in production at all &#8211; I’ve taken whatever
idiom I could find (or generate) for the internals of all of these, and haven’t
paid any attention to their performance. The goal was to make it easier for me
to work interactively in a REPL when I’m reaching for particular functions. That
being said, I’ll gladly do my best to understand the Pythonic versions as best
as I can so that I can better appreciate native Python and use the idioms when
my helper package isn’t available (or unsuitable). I’d say it’s fair to argue
that R users using Python <em>should</em> learn how to do things in a Pythonic way, but
I also just want to get some small things done occasionally, so I’m happy this
now exists.</p>
<p>If you’re working with non-R colleagues then introducing these abstractions —
while they may make your life simpler in the moment — will probably result in
confusion as you’re hiding away the implementation and giving it a name they
won’t recognise. That’s precisely what functions are for (with helpful names),
of course, but unless this package becomes popular, I’ll bet that the inline
idioms are more welcomed in a codebase.</p>
<p>I’d love to hear what people think about this, although I’m entirely fine with me
being the sole user of it. Should I just force my muscle-memory to take on the
Python idioms? Am I going to be punished for <a href="https://ghostbusters.fandom.com/wiki/Cross_the_Streams" rel="nofollow" target="_blank">‘crossing the
streams’</a> of two
incompatible languages? Would this be helpful to you? Are there other
considerations I’ve missed? As always, I can be found on
<a href="https://fosstodon.org/@jonocarroll" rel="nofollow" target="_blank">Mastodon</a> and the comment section below.</p>
<p>Shoutouts to Elio Campitelli and Michael Sumner for feedback on a draft of this
post.</p>
<br />
<details>
<summary>
<tt>devtools::session_info()</tt>
</summary>
<pre>## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.5.3 (2026-03-11)
##  os       macOS Tahoe 26.3.1
##  system   aarch64, darwin20
##  ui       X11
##  language (EN)
##  collate  en_US.UTF-8
##  ctype    en_US.UTF-8
##  tz       Australia/Adelaide
##  date     2026-05-22
##  pandoc   3.6.3 @ /Applications/RStudio.app/Contents/Resources/app/quarto/bin/tools/aarch64/ (via rmarkdown)
##  quarto   1.7.31 @ /usr/local/bin/quarto
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.23    2026-01-18 [1] CRAN (R 4.5.2)
##  bookdown      0.46    2025-12-05 [1] CRAN (R 4.5.2)
##  bslib         0.10.0  2026-01-26 [1] CRAN (R 4.5.2)
##  cachem        1.1.0   2024-05-16 [1] CRAN (R 4.5.0)
##  cli           3.6.5   2025-04-23 [1] CRAN (R 4.5.0)
##  devtools      2.4.6   2025-10-03 [1] CRAN (R 4.5.0)
##  digest        0.6.39  2025-11-19 [1] CRAN (R 4.5.2)
##  ellipsis      0.3.2   2021-04-29 [1] CRAN (R 4.5.0)
##  evaluate      1.0.5   2025-08-27 [1] CRAN (R 4.5.0)
##  fastmap       1.2.0   2024-05-15 [1] CRAN (R 4.5.0)
##  fs            1.6.7   2026-03-06 [1] CRAN (R 4.5.2)
##  glue          1.8.1   2026-04-17 [1] CRAN (R 4.5.2)
##  htmltools     0.5.9   2025-12-04 [1] CRAN (R 4.5.2)
##  jquerylib     0.1.4   2021-04-26 [1] CRAN (R 4.5.0)
##  jsonlite      2.0.0   2025-03-27 [1] CRAN (R 4.5.0)
##  knitr         1.51    2025-12-20 [1] CRAN (R 4.5.2)
##  lattice       0.22-9  2026-02-09 [1] CRAN (R 4.5.3)
##  lifecycle     1.0.5   2026-01-08 [1] CRAN (R 4.5.2)
##  magrittr      2.0.4   2025-09-12 [1] CRAN (R 4.5.0)
##  Matrix        1.7-4   2025-08-28 [1] CRAN (R 4.5.3)
##  memoise       2.0.1   2021-11-26 [1] CRAN (R 4.5.0)
##  otel          0.2.0   2025-08-29 [1] CRAN (R 4.5.0)
##  pkgbuild      1.4.8   2025-05-26 [1] CRAN (R 4.5.0)
##  pkgload       1.5.0   2026-02-03 [1] CRAN (R 4.5.2)
##  png           0.1-9   2026-03-15 [1] CRAN (R 4.5.2)
##  purrr         1.2.2   2026-04-10 [1] CRAN (R 4.5.2)
##  R6            2.6.1   2025-02-15 [1] CRAN (R 4.5.0)
##  Rcpp          1.1.1   2026-01-10 [1] CRAN (R 4.5.2)
##  remotes       2.5.0   2024-03-17 [1] CRAN (R 4.5.0)
##  reticulate    1.45.0  2026-02-13 [1] CRAN (R 4.5.2)
##  rlang         1.1.7   2026-01-09 [1] CRAN (R 4.5.2)
##  rmarkdown     2.30    2025-09-28 [1] CRAN (R 4.5.0)
##  rstudioapi    0.18.0  2026-01-16 [1] CRAN (R 4.5.2)
##  sass          0.4.10  2025-04-11 [1] CRAN (R 4.5.0)
##  sessioninfo   1.2.3   2025-02-05 [1] CRAN (R 4.5.0)
##  usethis       3.2.1   2025-09-06 [1] CRAN (R 4.5.0)
##  vctrs         0.7.1   2026-01-23 [1] CRAN (R 4.5.2)
##  withr         3.0.2   2024-10-28 [1] CRAN (R 4.5.0)
##  xfun          0.56    2026-01-18 [1] CRAN (R 4.5.2)
##  yaml          2.3.12  2025-12-10 [1] CRAN (R 4.5.2)
## 
##  [1] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
## 
## ─ Python configuration ───────────────────────────────────────────────────────
##  python:         /Users/jono/.cache/uv/archive-v0/Q1veGTfRq3GBaNYBXjagV/bin/python
##  libpython:      /Users/jono/.local/share/uv/python/cpython-3.12.12-macos-aarch64-none/lib/libpython3.12.dylib
##  pythonhome:     /Users/jono/.cache/uv/archive-v0/Q1veGTfRq3GBaNYBXjagV:/Users/jono/.cache/uv/archive-v0/Q1veGTfRq3GBaNYBXjagV
##  virtualenv:     /Users/jono/.cache/uv/archive-v0/Q1veGTfRq3GBaNYBXjagV/bin/activate_this.py
##  version:        3.12.12 (main, Oct 28 2025, 11:52:25) [Clang 20.1.4 ]
##  numpy:          /Users/jono/.cache/uv/archive-v0/Q1veGTfRq3GBaNYBXjagV/lib/python3.12/site-packages/numpy
##  numpy_version:  2.4.6
##  
##  NOTE: Python version was forced by VIRTUAL_ENV
## 
## ──────────────────────────────────────────────────────────────────────────────</pre>
</details>
<p><br /></p>
</div>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://jcarroll.com.au/2026/05/22/functions-over-idioms-rfuns/"> rstats on Irregularly Scheduled Programming</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/functions-over-idioms-writing-r-in-python-with-rfuns/">Functions over Idioms – Writing R in Python with rfuns</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401387</post-id>	</item>
		<item>
		<title>Zero Sum Problems</title>
		<link>https://www.r-bloggers.com/2026/05/zero-sum-problems/</link>
		
		<dc:creator><![CDATA[R on kieranhealy.org]]></dc:creator>
		<pubDate>Thu, 21 May 2026 19:50:40 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/</guid>

					<description><![CDATA[<p>Over at Daring Fireball, John Gruber makes a passing observation about the Apple Sports app:</p>
<p>    I’ve got some gripes about certain specific aspects of Apple Sports. Like, where does one even start to explain how much is wrong with their zero-sum v...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/zero-sum-problems/">Zero Sum Problems</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/"> R on kieranhealy.org</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>Over at <a href="https://daringfireball.net/" rel="nofollow" target="_blank">Daring Fireball</a>, John Gruber makes a passing observation about the Apple Sports app:</p>



<blockquote>
    <p>I’ve got some gripes about certain specific aspects of Apple Sports. Like, where does one even <em>start</em> to explain how much is wrong with <a href="https://daringfireball.net/misc/2026/05/apple-sports-team-stats-wtf.png" rel="nofollow" target="_blank">their zero-sum visualization of team stats</a>? Has anyone ever even seen a presentation like that before? <a href="https://kieranhealy.org/" rel="nofollow" target="_blank">Anyone</a>?</p>

</blockquote>

<p>That “Anyone” link lands over here. Hi everyone! The team stats image <em>is</em> quite confusing. It’s a summary of a game between the San Antonio Spurs and the Oklahoma City Thunder. I don’t know much about basketball, but I do know a bit about data visualization and in a pleasing coincidence my former student <a href="https://www.linkedin.com/in/joshua-fink" rel="nofollow" target="_blank">Josh Fink</a> is the A-VP of Basketball Data Science for the Spurs. Here is the image that John objected to:</p>
<figure><a href="https://i0.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/apple-sports-team-stats-wtf.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i0.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/apple-sports-team-stats-wtf.png?w=578&#038;ssl=1"
         alt="Confusing Apple Sports team stats visualization." data-recalc-dims="1"/></a><figcaption>
            <p>I had to look at it for a while as well.</p>
        </figcaption>
</figure>
<p>I just finished driving a very long way up the side of the country, so I’m kind of tired. But even allowing for that, boy, this way of representing things really is quite confusing. Not being an Apple Sports user I had to look at it for a bit to understand what was happening. But, now that it has given me a headache, I can kind of see why whoever designed this ended up in the undoubtedly bad place they did.</p>
<p>Before I get to why I have some sympathy for the designer, <em>why</em> did I find this representation of these numbers so disorienting? It’s not just just because I’ve been driving for nine hours. John is right to call the picture a “Zero Sum” representation. The design <em>strongly</em> suggests to the viewer that, within each row, we’re looking at each team’s share of a total. Each pair of black and blue lines seem to be vying for control of their whole row, with the longest line being the “winner” in each case.</p>
<p>This sort of representation would make perfect sense for a measure that really
<em>was</em> zero sum. Take an example from a properly good sport, like rugby. There,
like in basketball, to a first approximation a team either has the ball or it
doesn’t.<sup id="fnref:1"><a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/#fn:1" class="footnote-ref" role="doc-noteref" rel="nofollow" target="_blank">1</a></sup> But there’s no shot clock in rugby, and possession routinely gets
turned over without the game stopping. So, knowing that Team A had 65%
possession is not only informative, it also immediately entails that Team B had
35%. You could show that with a representation like one of the rows above.</p>
<p>Literally none of the measures in the Basketball data above are zero-sum in this way. Both teams could shoot 100% from the free throw line, or zero percent. But because the first three measures shown are percentages, this reinforces the zero-sum impression given by the lines. It certainly did that in my case. But then, starting with Assists, the remaining rows are just absolute numbers. When I started looking at the absolute numbers, I got confused a second time by the length of the lines. “Oh so it’s not a share, it’s the value” I thought—but no, they do correspond in terms of relative proportions to the teams share within each row. But they’re not really <em>shares</em> they’re just <em>magnitudes</em>. But they have to be shown in a fixed space and we want to make them relatively comparable somehow so …  Argh.</p>
<p>It would be nice if there were One Weird Trick to fully fix this figure. But I’m not sure that there is. For example, at a minimum we could redraw these numbers to reflect the fact that they’re not zero-sum. Keep each measure as a row (i.e. on the y-axis) but have the lines, or columns, be side by side within each category instead of facing off. Like this:</p>
<figure><a href="https://i1.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats1.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i1.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats1.png?w=578&#038;ssl=1"
         alt="Team Stats side by side for each measure." data-recalc-dims="1"/></a><figcaption>
            <p>Team Stats side by side for each measure.</p>
        </figcaption>
</figure>
<p>This view at least lets you immediately see who “won” each measure. The viewer
can just directly compare the length of the bars in each category. <a href="https://socviz.co/01-look-at-data.html#visual-tasks-and-decoding-graphs" rel="nofollow" target="_blank">People are
really good at doing that
accurately.</a>
In that sense it’s much less confusing than the original. But there’s still a
lot wrong with it. The core problem is that when we draw a graph like this,
we’re usually putting <em>the same kind of thing</em> (e.g. countries, or religious
groups, or sports teams) on the y-axis, and then seeing how different their
scores are on some single measure (e.g. GDP, or number of adherents, or average
points scored per game), which we put on the x-axis. Maybe we use color to break
things out by some third measure as well.<sup id="fnref:2"><a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/#fn:2" class="footnote-ref" role="doc-noteref" rel="nofollow" target="_blank">2</a></sup> In
this case, I’ve just labeled the x-axis as generically as possible. “Value”
covers the range of all the measures. The lowest value is 5, in Largest Lead.
The highest is 88, in Free Throw %. But these numbers are not meaningfully
comparable. The graph encourages us to compare across as well as within
categories. But while within-category comparisons are meaningful, the
between-category ones are not. There were way more Bench Points than Blocks in
the game. But that is not a useful thing to know.</p>
<p>Knowing who won each measure isn’t nothing. It can be informative about how the game went, maybe especially when a team won the game but “lost” on a number of the measures. If you really wanted to lean in to that aspect, you could sort of justify the zero-sum view, and maybe look for a way to sort and order by “how much” a team “won” each category. But again, what’s the right denominator for those measures? For instance, do we care about a team’s share of all Defensive Rebounds in the game? Or do we care about the share of Defensive Rebounds a team won relative to every opportunity it had to make a Defensive Rebound? How meaningful is ordering our rows by those kinds of shares? Even worse, some measures (notably Fouls) are <em>bad</em> to “win”, so we’d have to do something about those.</p>
<figure><a href="https://i2.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats2.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i2.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats2.png?w=578&#038;ssl=1"
         alt="Team Stats side by side and ordered from absolute highest to lowest, whatever that means." data-recalc-dims="1"/></a><figcaption>
            <p>Team Stats side by side and ordered from absolute highest to lowest, whatever that means.</p>
        </figcaption>
</figure>
<p>Our fundamental problem is that we just have two cases (the teams) and fifteen
different measures, or variables. Each variable, except for the three
percentages, is in effect on its own scale. There’s no direct way to make
comparisons across them. Sure, some of these measures are probably going to be
associated with one another—e.g. Turnovers and Points Off Turnovers—but the
numeric values aren’t directly comparable in general. If you know a lot about
basketball you might have some informative rules of thumb about each one of
these measures, or some of them in combination. But at that point the lines in
this particular graph are not going to be doing any work for you; you’ll just
end up looking directly at the numbers. If we had data on all these measures for
every NBA game for a whole season then we could of course do much more with
them, because then each measure would have a distribution across all games and
across all teams.</p>
<p>As it is, the purpose of the “Stats” screen in Apple Sports is just to summarize
information from a single game. The other thing I could think of to do with the
numbers as kind of graph is something like this:</p>
<figure><a href="https://i1.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats3.png?ssl=1" rel="nofollow" target="_blank">
    <img src="https://i1.wp.com/kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/gruber-stats3.png?w=578&#038;ssl=1"
         alt="A back-to-back column chart." data-recalc-dims="1"/></a><figcaption>
            <p>A back-to-back column chart.</p>
        </figcaption>
</figure>
<p>This is <em>marginally</em> more helpful than the one before just because, again, it
gets rid of the unhelpful zero-sum look of the original. As I hope you can
immediately see, it creates many other difficulties. It also doesn’t do away
with the core problem. That problem is principally one of information design
rather than data visualization. What I mean is that what we’re trying to
organize is, in effect, fifteen pairs of related but fundamentally distinct
numbers. If we had fifteen <em>cases</em> and two <em>variables</em> things would be simple. But
with fifteen variables and two cases … well, this is not the kind of thing you
can make a single effective and non-confusing graph out of. That’s why I kind of
sympathize with the designer. In a constrained space they have to show thirty
numbers (thirty two, including the score). Lots of information. A straight table
seems like it would be boring. Surely there’s some way to thematically integrate
the numbers in a visually appealing manner that brings out some of the
relationships across the rows. That’s what graphs do; it seems like the right
thing to reach for. But at its heart this information is not a graph. It just
sort of looks like one, and that ends up confusing people.</p>
<div class="footnotes" role="doc-endnotes">
<hr>
<ol>
<li id="fn:1">
<p>Modulo some measurement decisions about how to determine when possession is turned over while the ball is in play. <a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/#fnref:1" class="footnote-backref" role="doc-backlink" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p>
</li>
<li id="fn:2">
<p><a href="https://socviz.co/05-more-on-geoms.html#fig-ch-05-organdata-06" rel="nofollow" target="_blank">Here’s an
example</a> of a graph with a categorical measure on the y-axis, a continuous measure on the x-axis, and an additional categorical feature shown with color. <a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/#fnref:2" class="footnote-backref" role="doc-backlink" rel="nofollow" target="_blank"><img src="https://s.w.org/images/core/emoji/13.0.0/72x72/21a9.png" alt="↩" class="wp-smiley" style="height: 1em; max-height: 1em;" />︎</a></p>
</li>
</ol>
</div>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://kieranhealy.org/blog/archives/2026/05/21/zero-sum-problems/"> R on kieranhealy.org</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/zero-sum-problems/">Zero Sum Problems</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401384</post-id>	</item>
		<item>
		<title>Conformalized TabICL: Prediction Intervals for a State-Of-The-Art Tabular Foundation Model in Python and R</title>
		<link>https://www.r-bloggers.com/2026/05/conformalized-tabicl-prediction-intervals-for-a-state-of-the-art-tabular-foundation-model-in-python-and-r/</link>
		
		<dc:creator><![CDATA[T. Moudiki]]></dc:creator>
		<pubDate>Thu, 21 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://thierrymoudiki.github.io//blog/2026/05/21/r/python/Conformalized-TabICL-nnetsauce</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Prediction Intervals for Tabular Regression in Python and R via Conformalized TabICL; comparison with RidgeCV</div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/conformalized-tabicl-prediction-intervals-for-a-state-of-the-art-tabular-foundation-model-in-python-and-r/">Conformalized TabICL: Prediction Intervals for a State-Of-The-Art Tabular Foundation Model in Python and R</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://thierrymoudiki.github.io//blog/2026/05/21/r/python/Conformalized-TabICL-nnetsauce"> T. Moudiki's Webpage - R</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>A few days ago, I presented <a href="https://thierrymoudiki.github.io/blog/2026/05/17/r/python/conformalized-tabpfn" rel="nofollow" target="_blank">Conformalized TabPFN: Prediction Intervals for a Pretrained Transformer for Tabular Data in Python and R</a>. Today, it’s about <a href="https://github.com/soda-inria/tabicl" rel="nofollow" target="_blank">TabICL</a>, another state-of-the-art tabular foundation model. <code>TabICL</code> requires no token, as you’ll notice in the following Python and R code.</p>

<h1 id="1---python-version">1 &#8211; Python version</h1>

<pre>!pip install tabicl nnetsauce # scikit-learn matplotlib numpy

from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from sklearn.linear_model import RidgeCV
from sklearn.metrics import mean_squared_error
from tabicl import TabICLRegressor
import nnetsauce as ns
import numpy as np
import matplotlib.pyplot as plt
from time import time

# ── data ───────────────────────────────────────────────────
X, y = load_diabetes(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# ── base models ────────────────────────────────────────────
models = {
    &quot;TabICL&quot;: TabICLRegressor(),
    &quot;RidgeCV&quot;: RidgeCV(),
}

results = {}
for name, reg in models.items():
    start = time()
    conf = ns.PredictionInterval(reg, level=95)
    conf.fit(X_train, y_train)
    pi = conf.predict(X_test, return_pi=True)
    print(f&quot;{name:10s}  time={time() - start:.1f}s&quot;)

    coverage = np.mean((pi.lower &lt;= y_test) &#038; (pi.upper &gt;= y_test))
    width    = np.mean(pi.upper - pi.lower)
    rmse     = np.sqrt(mean_squared_error(y_test, pi.mean))

    results[name] = {&quot;pi&quot;: pi, &quot;coverage&quot;: coverage,
                     &quot;width&quot;: width, &quot;rmse&quot;: rmse}
    print(f&quot;{name:10s}  RMSE={rmse:.1f}  &quot;
          f&quot;coverage={coverage:.3f}  avg_width={width:.1f}&quot;)

# ── plot side-by-side ──────────────────────────────────────
fig, axes = plt.subplots(1, 2, figsize=(12, 4), sharey=True)
colors = {&quot;TabICL&quot;: &quot;orange&quot;, &quot;RidgeCV&quot;: &quot;steelblue&quot;}
max_idx = 50

for ax, (name, res) in zip(axes, results.items()):
    pi = res[&quot;pi&quot;]
    x  = range(max_idx)
    ax.fill_between(x, pi.lower[:max_idx], pi.upper[:max_idx],
                     alpha=0.35, color=colors[name], label=&quot;95% PI&quot;)
    ax.plot(x, pi.mean[:max_idx], &quot;k--&quot;, lw=1.5, label=&quot;predicted&quot;)
    ax.plot(x, y_test[:max_idx], &quot;k.&quot;, ms=6, alpha=0.4, label=&quot;observed&quot;)
    ax.set_title(
        f&quot;{name}  |  cov={res['coverage']:.3f}  width={res['width']:.1f}&quot;
    )
    ax.legend(fontsize=8)

plt.suptitle(&quot;Conformalized TabICL vs RidgeCV — diabetes dataset&quot;)
plt.tight_layout()
plt.show()

Checkpoint 'tabicl-regressor-v2-20260212.ckpt' not cached.
 Downloading from Hugging Face Hub (jingang/TabICL).




tabicl-regressor-v2-20260212.ckpt:   0%|          | 0.00/114M [00:00&lt;?, ?B/s]


TabICL      time=21.8s
TabICL      RMSE=54.4  coverage=0.955  avg_width=226.1
RidgeCV     time=0.0s
RidgeCV     RMSE=53.9  coverage=0.955  avg_width=211.5
</pre>

<p><img src="https://i2.wp.com/thierrymoudiki.github.io/images/2026-05-21/2026-05-21-Conformalized-TabICL-nnetsauce_3_3.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<h1 id="2---r-version">2 - R version</h1>

<pre> %load_ext rpy2.ipython # in a Colab notebook, use this

%R install.packages(&quot;reticulate&quot;)

%%R  # in Colab/Jupyter with rpy2; remove this line for pure R

library(reticulate)

# pip install tabicl nnetsauce scikit-learn matplotlib numpy

sklearn_ds  &lt;- import(&quot;sklearn.datasets&quot;)
sklearn_ms  &lt;- import(&quot;sklearn.model_selection&quot;)
sklearn_m   &lt;- import(&quot;sklearn.metrics&quot;)
sklearn_lm  &lt;- import(&quot;sklearn.linear_model&quot;)
tabicl      &lt;- import(&quot;tabicl&quot;)
ns          &lt;- import(&quot;nnetsauce&quot;)
np          &lt;- import(&quot;numpy&quot;)
plt         &lt;- import(&quot;matplotlib.pyplot&quot;)

# ── data ───────────────────────────────────────────────────
d       &lt;- sklearn_ds$load_diabetes(return_X_y = TRUE)
X &lt;- d[[1]]; y &lt;- d[[2]]
sp      &lt;- sklearn_ms$train_test_split(X, y,
             test_size = 0.2, random_state = 42L)
X_train &lt;- sp[[1]]; X_test &lt;- sp[[2]]
y_train &lt;- sp[[3]]; y_test &lt;- sp[[4]]

# ── helper: fit + evaluate ─────────────────────────────────
eval_model &lt;- function(reg, name) {
  conf &lt;- ns$PredictionInterval(reg, level = 95L)
  conf$fit(X_train, y_train)
  pi   &lt;- conf$predict(X_test, return_pi = TRUE)

  cov  &lt;- np$mean((pi$lower &lt;= y_test) * (pi$upper &gt;= y_test))
  wid  &lt;- np$mean(pi$upper - pi$lower)
  rmse &lt;- sqrt(sklearn_m$mean_squared_error(y_test, pi$mean))

  cat(sprintf(&quot;%-10s  RMSE=%.1f  coverage=%.3f  avg_width=%.1f\n&quot;,
              name, rmse, cov, wid))
  invisible(pi)
}

# ── run both models ────────────────────────────────────────
pi_tabicl  &lt;- eval_model(tabicl$TabICLRegressor(),  &quot;TabICL&quot;)
pi_ridge   &lt;- eval_model(sklearn_lm$RidgeCV(),       &quot;RidgeCV&quot;)

# ── plot ───────────────────────────────────────────────────
max_idx &lt;- 50L
x_range &lt;- np$array(0:(max_idx - 1))

plot_pi &lt;- function(pi, title, col) {
  x_fill &lt;- np$concatenate(list(x_range, x_range[max_idx:1]))
  y_fill &lt;- np$concatenate(list(
    pi$upper[1:max_idx], pi$lower[max_idx:1]))
  plt$fill(x_fill, y_fill, alpha=0.35, fc=col, ec=&quot;None&quot;, label=&quot;95% PI&quot;)
  plt$plot(x_range, pi$mean[1:max_idx], &quot;k--&quot;, lw=1.5, label=&quot;predicted&quot;)
  plt$plot(x_range, y_test[1:max_idx], &quot;k.&quot;, ms=6L, alpha=0.4, label=&quot;observed&quot;)
  plt$title(title); plt$legend(fontsize=8L)
}

fig &lt;- plt$figure(figsize=c(12, 4))
plt$subplot(1L, 2L, 1L); plot_pi(pi_tabicl, &quot;Conformalized TabICL&quot;, &quot;orange&quot;)
plt$subplot(1L, 2L, 2L); plot_pi(pi_ridge,  &quot;Conformalized RidgeCV&quot;, &quot;steelblue&quot;)
plt$suptitle(&quot;Conformalized TabICL vs RidgeCV — diabetes dataset&quot;)
plt$tight_layout()
plt$show()

    WARNING: The R package &quot;reticulate&quot; only fixed recently
    an issue that caused a segfault when used with rpy2:
    https://github.com/rstudio/reticulate/pull/1188
    Make sure that you use a version of that package that includes
    the fix.
    TabICL      RMSE=54.4  coverage=0.955  avg_width=226.1
RidgeCV     RMSE=53.9  coverage=0.955  avg_width=211.5
</pre>

<p><img src="https://i2.wp.com/thierrymoudiki.github.io/images/2026-05-21/2026-05-21-Conformalized-TabICL-nnetsauce_7_1.png?w=578&#038;ssl=1" alt="image-title-here" class="img-responsive" data-recalc-dims="1" /></p>

<p>Probably a dataset that’s too <em>easy</em> for a Transformer. Conformalizing simple models helps them, in general, to obtain coverage rates close to the nominal level, as we see for RidgeCV here.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://thierrymoudiki.github.io//blog/2026/05/21/r/python/Conformalized-TabICL-nnetsauce"> T. Moudiki's Webpage - R</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/conformalized-tabicl-prediction-intervals-for-a-state-of-the-art-tabular-foundation-model-in-python-and-r/">Conformalized TabICL: Prediction Intervals for a State-Of-The-Art Tabular Foundation Model in Python and R</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401359</post-id>	</item>
		<item>
		<title>The Atlas-Learn Approach to the Manifold Hypothesis</title>
		<link>https://www.r-bloggers.com/2026/05/the-atlas-learn-approach-to-the-manifold-hypothesis/</link>
		
		<dc:creator><![CDATA[R Works]]></dc:creator>
		<pubDate>Wed, 20 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://rworks.dev/posts/atlas-learn-sphere/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>The manifold hypothesis, the idea that real-world high-dimensional data concentrates near a low-dimensional curved subspace, is foundational to modern machine learning. Many popular manifold learning methods such as UMAP, t-SNE, Isomap, and diff...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/the-atlas-learn-approach-to-the-manifold-hypothesis/">The Atlas-Learn Approach to the Manifold Hypothesis</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://rworks.dev/posts/atlas-learn-sphere/"> R Works</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 





<p>The <strong>manifold hypothesis</strong>, the idea that real-world high-dimensional data concentrates near a low-dimensional curved subspace, is foundational to modern machine learning. Many popular manifold learning methods such as UMAP, t-SNE, Isomap, and diffusion maps do achieve dimensionality reduction by embedding data into a flat Euclidean space <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5ED"/>, but they do not attempt to directly learn the underlying manifold. In contrast, the 2025 paper by <a href="https://arxiv.org/pdf/2510.17772" rel="nofollow" target="_blank">Robinett et al.</a>, <em>Atlas-based manifold representations for interpretable Riemannian machine learning</em>, offers a proof of concept for directly tackling the manifold hypothesis based on fundamental ideas from differential geometry. It provides an algorithm for learning a low dimensional manifold from point data by constructing an atlas of charts. The paper is also notable for the design of an efficient data structure for working with the learned atlas and for the extensive supplementary materials that include a <a href="https://anonymous.4open.science/r/atlas_graph_learning-6DE0" rel="nofollow" target="_blank">GitHub Repository</a> containing several practical Python algorithms for doing calculations on manifolds, and an extraordinary amount of implementation detail.</p>
<p>Reading through Robinett et al., however, requires a fairly deep background in the theory of differential geometry. This post is an attempt to provide an on-ramp to Robinett et al. by discussing the relatively simple example of the two dimensional sphere, <img src="https://latex.codecogs.com/png.latex?S%5E2"/> embedded in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%5E3%7D"/>. It implements the <strong>Atlas-Learn</strong> data structures and algorithms in <code>R</code>, uses them to learn <img src="https://latex.codecogs.com/png.latex?S%5E2"/> and then goes on to validate the Atlas-Learn algorithm for the sphere via three independent methods: 1) use numerical integration along the manifold to trace a great circle on the sphere, 3) recover the radius of curvature of the sphere from the atlas, and 4) verify the Gauss-Bonnet Theorem for the sphere.</p>
<p>The <code>R</code> code was mostly worked out by Claude Sonnet 4.3 in the context of participating in the Posit beta test for its AI Assistant. I found the integration of the AI engine into the RStudio IDE an effective means of communicating with Claude and managing the project workflow.</p>
<section id="atlas-learn-theory-and-algorithm" class="level1">
<h1>Atlas-Learn: Theory and Algorithm</h1>
<p>This section provides some minimal theoretical background for understanding the Atlas-Learn algorithm. A <em>smooth manifold</em> <img src="https://latex.codecogs.com/png.latex?%5Cmathcal%7BM%7D"/> of intrinsic dimension <img src="https://latex.codecogs.com/png.latex?d"/> embedded in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5ED"/> can be described by an <strong>atlas</strong> — a finite collection of <em>charts</em> <img src="https://latex.codecogs.com/png.latex?%5C%7B(%5Cvarphi_j,%20%5COmega_j)%5C%7D_%7Bj=1%7D%5E%7Bk%7D"/> such that the open sets <img src="https://latex.codecogs.com/png.latex?U_j%20=%20%5Cvarphi_j%5E%7B-1%7D(%5COmega_j)"/> cover <img src="https://latex.codecogs.com/png.latex?%5Cmathcal%7BM%7D"/> and each chart map <img src="https://latex.codecogs.com/png.latex?%5Cvarphi_j%20:%20U_j%20%5Cto%20%5Cmathbb%7BR%7D%5Ed"/> is a smooth bijection onto its image.</p>
<p>Normally, the definition of a smooth manifold also requires that any two charts be smoothly compatible, where two charts <img src="https://latex.codecogs.com/png.latex?(%5Cmathcal%7BU%7D_1,%20%5Cphi_1)"/> and <img src="https://latex.codecogs.com/png.latex?(%5Cmathcal%7BU%7D_2,%20%5Cphi_2)"/> are said to be iff <img src="https://latex.codecogs.com/png.latex?%5Cphi_1(%5Cmathcal%7BU%7D_1%20%5Ccap%20%5Cmathcal%7BU%7D_2)"/> and <img src="https://latex.codecogs.com/png.latex?%5Cphi_2(%5Cmathcal%7BU%7D_1%20%5Ccap%20%5Cmathcal%7BU%7D_2)"/> are both open in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5Em"/> and the transition map <img src="https://latex.codecogs.com/png.latex?%5Cphi_%7B21%7D%20=%20%5Cphi_2%20%5Ccirc%20%5Cphi_1%5E%7B-1%7D%20:%20%5Cphi_1(%5Cmathcal%7BU%7D_1%20%5Ccap%20%5Cmathcal%7BU%7D_2)%20%5Cto%20%5Cphi_2(%5Cmathcal%7BU%7D_1%20%5Ccap%20%5Cmathcal%7BU%7D_2)"/> is a diffeomorphism (e.g. see [2]). Robinett et al. relax the smoothly compatible requirement and define transition maps <img src="https://latex.codecogs.com/png.latex?%5Cpsi_%7Bij%7D"/> separately from coordinate chart images. They then approximate a differentiable atlas by ensuring that the discrepancy between coordinate charts and transition maps <img src="https://latex.codecogs.com/png.latex?%7C%7C%7B%5Cphi(%5Cxi)%20-%20%5Cphi(%5Cpsi(xi)))%7D%7C%7C_%5Cmathbb%7BR%5ED%7D"/> goes to 0 as the number of charts and the number of points sampled goes to infinity.</p>
<p>In the Atlas-Learn algorithm the manifold is a surface (<img src="https://latex.codecogs.com/png.latex?d%20=%202"/>) embedded in <img src="https://latex.codecogs.com/png.latex?%5Cmathbb%7BR%7D%5E3"/>, and both the covering sets and the chart maps are <em>learned</em> from a finite point cloud <img src="https://latex.codecogs.com/png.latex?%5C%7Bx_i%5C%7D_%7Bi=1%7D%5E%7BN%7D%20%5Csubset%20%5Cmathcal%7BM%7D"/>. The algorithm proceeds to construct an atlas in four basic steps.</p>
<p>The Atlas-Learn algorithm proceeds in four steps for each chart:</p>
<ol type="1">
<li>The point cloud comprising the data, the sphere in our case, is partitioned into k-medoids.</li>
<li>Local PCA is used to find the tangent plane and the normal plane for each point.</li>
<li>Quadratic regression is performed to find the curvature coefficients, K</li>
<li>The minimum ellipsoidal region enclosing the chart is estimated.</li>
</ol>
</section><section id="step-1-partitioning-via-k-medoids" class="level2">
<h2 class="anchored" data-anchor-id="step-1-partitioning-via-k-medoids">Step 1: Partitioning via k-medoids</h2>
<p>The point cloud is partitioned into <img src="https://latex.codecogs.com/png.latex?k"/> clusters using the <img src="https://latex.codecogs.com/png.latex?k"/>-medoids algorithm (PAM). Unlike <img src="https://latex.codecogs.com/png.latex?k"/>-means, PAM selects actual data points as cluster centers (medoids), which makes the partition robust to outliers and avoids projection artefacts. Each point <img src="https://latex.codecogs.com/png.latex?x_i"/> receives a chart label <img src="https://latex.codecogs.com/png.latex?j(i)%20%5Cin%20%5C%7B1,%5Cldots,k%5C%7D"/>, and the points belonging to chart <img src="https://latex.codecogs.com/png.latex?j"/> together with their centroid are</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cmathcal%7BX%7D_j%20=%20%5C%7B%20x_i%20:%20j(i)%20=%20j%20%5C%7D,%20%5Cqquad%0A%5Cbar%7Bx%7D_j%20=%20%5Cfrac%7B1%7D%7B%7C%5Cmathcal%7BX%7D_j%7C%7D%20%5Csum_%7Bx%20%5Cin%20%5Cmathcal%7BX%7D_j%7D%20x.%0A"/></p>
</section>
<section id="step-2-local-pca-and-tangent-plane-estimation" class="level2">
<h2 class="anchored" data-anchor-id="step-2-local-pca-and-tangent-plane-estimation">Step 2: Local PCA and tangent-plane estimation</h2>
<p>For each cluster <img src="https://latex.codecogs.com/png.latex?j"/>, the centered data matrix <img src="https://latex.codecogs.com/png.latex?%5Cwidetilde%7BX%7D_j%20=%20%5Cmathcal%7BX%7D_j%20-%20%5Cbar%7Bx%7D_j%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B%7C%5Cmathcal%7BX%7D_j%7C%20%5Ctimes%203%7D"/> is decomposed via the thin SVD:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cwidetilde%7BX%7D_j%20=%20U%20%5CSigma%20V%5E%5Ctop,%20%5Cqquad%20V%20=%20%5Bv_1%20%5Cmid%20v_2%20%5Cmid%20v_3%5D.%0A"/></p>
<p>The first two right singular vectors span the <strong>local tangent plane</strong>:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AL_j%20=%20%5Bv_1%20%5Cmid%20v_2%5D%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B3%20%5Ctimes%202%7D,%0A"/></p>
<p>while the third singular vector <img src="https://latex.codecogs.com/png.latex?m_j%20=%20v_3%20%5Cin%20%5Cmathbb%7BR%7D%5E3"/> estimates the <strong>local surface normal</strong> (the direction of least variance). Each centered point is then decomposed into tangent and normal components:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Ctau_i%20=%20%5Cwidetilde%7Bx%7D_i%5E%5Ctop%20L_j%20%5Cin%20%5Cmathbb%7BR%7D%5E2,%20%5Cqquad%0A%5Cnu_i%20%20=%20%5Cwidetilde%7Bx%7D_i%5E%5Ctop%20m_j%20%5Cin%20%5Cmathbb%7BR%7D.%0A"/></p>
</section>
<section id="step-3-quadratic-chart-map" class="level2">
<h2 class="anchored" data-anchor-id="step-3-quadratic-chart-map">Step 3: Quadratic chart map</h2>
<p>On a smooth surface the normal offset <img src="https://latex.codecogs.com/png.latex?%5Cnu"/> is a smooth function of the tangent coordinates <img src="https://latex.codecogs.com/png.latex?%5Ctau"/>. Atlas-Learn approximates this by a <strong>degree-2 polynomial</strong> (capturing local curvature):</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cnu%20%5Capprox%20K_j%20%5C,%20%5Cphi(%5Ctau),%20%5Cqquad%0A%5Cphi(%5Ctau)%20=%20%5Cbigl%5B%5Ctau_1%5E2,%5C;%20%5Ctau_1%5Ctau_2,%5C;%20%5Ctau_2%5E2%5Cbigr%5D%5E%5Ctop,%0A"/></p>
<p>where <img src="https://latex.codecogs.com/png.latex?K_j%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B1%20%5Ctimes%203%7D"/> is estimated by ordinary least squares with a small ridge penalty <img src="https://latex.codecogs.com/png.latex?%5Cvarepsilon%20=%2010%5E%7B-10%7D"/>:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AK_j%20=%20%5Cboldsymbol%7B%5Cnu%7D%5E%5Ctop%20%5CPhi%0A%20%20%20%20%20%20%5Cbigl(%5CPhi%5E%5Ctop%20%5CPhi%20+%20%5Cvarepsilon%20I_3%5Cbigr)%5E%7B-1%7D,%20%5Cqquad%0A%5CPhi%20=%20%5Cbigl%5B%5Cphi(%5Ctau_1)%20%5C;%5Ccdots%5C;%20%5Cphi(%5Ctau_%7B%7C%5Cmathcal%7BX%7D_j%7C%7D)%5Cbigr%5D%5E%5Ctop%0A%20%20%20%20%20%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B%7C%5Cmathcal%7BX%7D_j%7C%20%5Ctimes%203%7D.%0A"/></p>
<p>The resulting <strong>inverse chart map</strong> <img src="https://latex.codecogs.com/png.latex?%5Cvarphi_j%5E%7B-1%7D%20:%20%5Cmathbb%7BR%7D%5E2%20%5Cto%20%5Cmathbb%7BR%7D%5E3"/> reconstructs an ambient point from local coordinates <img src="https://latex.codecogs.com/png.latex?%5Cxi%20%5Cin%20%5Cmathbb%7BR%7D%5E2"/>:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5Cvarphi_j%5E%7B-1%7D(%5Cxi)%20%5C;=%5C;%20%5Cbar%7Bx%7D_j%20+%20L_j%5C,%5Cxi%20+%20m_j%5C,K_j%5C,%5Cphi(%5Cxi).%0A"/></p>
<p>Its <strong>Jacobian</strong> <img src="https://latex.codecogs.com/png.latex?J_j(%5Cxi)%20=%20%5Cpartial%5Cvarphi_j%5E%7B-1%7D/%5Cpartial%5Cxi%20%5Cin%20%5Cmathbb%7BR%7D%5E%7B3%20%5Ctimes%202%7D"/>, required for geodesic integration, is:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AJ_j(%5Cxi)%20%5C;=%5C;%20L_j%20%5C;+%5C;%20m_j%5C,K_j%5C,%5Cfrac%7B%5Cpartial%5Cphi%7D%7B%5Cpartial%5Cxi%7D(%5Cxi),%20%5Cqquad%0A%5Cfrac%7B%5Cpartial%5Cphi%7D%7B%5Cpartial%5Cxi%7D(%5Cxi)%20=%20%5Cbegin%7Bpmatrix%7D%0A%20%202%5Cxi_1%20&#038;%200%20%20%20%20%20%20%5C%5C%0A%20%20%5Cxi_2%20%20&#038;%20%5Cxi_1%20%20%5C%5C%0A%20%200%20%20%20%20%20%20&#038;%202%5Cxi_2%0A%5Cend%7Bpmatrix%7D.%0A"/></p>
</section>
<section id="step-4-ellipsoidal-chart-domains" class="level2">
<h2 class="anchored" data-anchor-id="step-4-ellipsoidal-chart-domains">Step 4: Ellipsoidal chart domains</h2>
<p>Each chart is assigned an <strong>ellipsoidal domain</strong> <img src="https://latex.codecogs.com/png.latex?%5COmega_j%20%5Csubset%20%5Cmathbb%7BR%7D%5E2"/> defined by</p>
<p><img src="https://latex.codecogs.com/png.latex?%0A%5COmega_j%20=%20%5Cbigl%5C%7B%5C,%20%5Cxi%20%5Cin%20%5Cmathbb%7BR%7D%5E2%20:%20%5Cxi%5E%5Ctop%20A_j%5C,%5Cxi%20%5Cleq%201%20%5C,%5Cbigr%5C%7D,%0A"/></p>
<p>where <img src="https://latex.codecogs.com/png.latex?A_j"/> is a rescaled inverse covariance of the projected points:</p>
<p><img src="https://latex.codecogs.com/png.latex?%0AA_j%20=%20%5Cfrac%7B%5Coperatorname%7BCov%7D(%5Ctau)%5E%7B-1%7D%7D%0A%20%20%20%20%20%20%20%20%20%20%20%7B%5Crho%20%5Ccdot%20%5Cmax_i%20%5Cbigl%5C%7B%5Ctau_i%5E%5Ctop%5Coperatorname%7BCov%7D(%5Ctau)%5E%7B-1%7D%5Ctau_i%5Cbigr%5C%7D%7D,%0A%5Cqquad%20%5Crho%20=%20%5Ctexttt%7Bellipsoid%5C_scale%7D.%0A"/></p>
<p>Setting <img src="https://latex.codecogs.com/png.latex?%5Crho%20%3E%201"/> (default <img src="https://latex.codecogs.com/png.latex?%5Crho%20=%201.1"/>) inflates each domain slightly beyond the convex hull of its own cluster, so that neighboring charts <strong>overlap</strong> and transitions are always possible. On <img src="https://latex.codecogs.com/png.latex?S%5E2"/> specifically, because the sphere is isotropic and the <img src="https://latex.codecogs.com/png.latex?k"/>-medoids partition tends to produce roughly equal-area, near-circular patches, the learned ellipsoids <img src="https://latex.codecogs.com/png.latex?%5COmega_j"/> are close to circles (<img src="https://latex.codecogs.com/png.latex?A_j%20%5Capprox%20%5Clambda%20I_2"/> for some scalar <img src="https://latex.codecogs.com/png.latex?%5Clambda"/>). Each chart is assigned a domain <img src="https://latex.codecogs.com/png.latex?%5COmega_j%20=%20%5C%7B%5Cxi%20%5Cin%20%5Cmathbb%7BR%7D%5E2%20:%20%5Cxi%5E%5Ctop%20A_j%5Cxi%20%5Cleq%201%5C%7D"/> where <img src="https://latex.codecogs.com/png.latex?A_j"/> is a rescaled inverse covariance of the projected tangent-plane coordinates. Setting the scale factor <img src="https://latex.codecogs.com/png.latex?%5Crho%20%3E%201"/> (default <img src="https://latex.codecogs.com/png.latex?%5Crho%20=%201.1"/>) inflates domains slightly so that neighboring charts overlap and transitions are always possible.</p>
<hr />
</section>

<section id="the-atlas-learn-implementation" class="level1">
<h1>The Atlas-Learn Implementation</h1>
<p>Here are the required <code>R</code> packages.</p>
<div class="cell">
<details class="code-fold">
<summary>Required Packages</summary>
<pre>library(tidyverse)
library(cluster)    # pam() for k-medoids partitioning
library(RANN)       # nn2() for fast k-nearest-neighbor queries
library(plotly)     # interactive 3D visualization
library(purrr)      # map() / imap() for list operations</pre>
</details>
</div>
<p>This block of code contains all of the functions for the Atlas Learn implementation.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>#| label: atlas-functions

# ===========================================================================
# PART 1: Quadratic feature helpers
# ---------------------------------------------------------------------------
# These functions implement the d=2 specialization of the general quadratic
# feature map. For general d, phi(xi) would have choose(d+1, 2) components.
# For d=2 it has exactly 3: [xi1^2, xi1*xi2, xi2^2].
# ===========================================================================

# Maps xi in R^2 to the three quadratic monomials used to model surface curvature.
# General d would give choose(d+1,2) monomials; for d=2 this is exactly 3.
quad_features &lt;- function(xi) {
  c(xi[1]^2, xi[1] * xi[2], xi[2]^2)
}

# Jacobian of quad_features w.r.t. xi: a 3x2 matrix (d=2 specialization).
# Row i is the gradient of the i-th monomial:
#   d/dxi [xi1^2]     = [2*xi1,   0   ]
#   d/dxi [xi1*xi2]   = [xi2,     xi1 ]
#   d/dxi [xi2^2]     = [0,       2*xi2]
quad_jacobian &lt;- function(xi) {
  matrix(
    c(2 * xi[1],   xi[2],         0,
              0,   xi[1],  2 * xi[2]),
    nrow = 3, ncol = 2, byrow = TRUE
  )
}

# ===========================================================================
# PART 2: Core chart operations
# ---------------------------------------------------------------------------
# Each chart stores:
#   mean  : R^3 — chart center (centroid of the cluster)
#   L     : R^{3x2} — orthonormal tangent basis (columns = v1, v2 from SVD)
#   M     : R^{3x1} — unit surface normal (v3 from SVD; scalar normal because D-d=1)
#   K     : R^{1x3} — quadratic curvature coefficients (1 row because D-d=1)
#   ell_A : R^{2x2} — ellipsoidal domain matrix (2x2 because d=2)
# ===========================================================================

# Evaluate the inverse chart map: xi in R^2  -&gt;  point in R^3.
# Formula: x = mean + L*xi + M * (K * phi(xi))
#   - mean + L*xi : linear move along the tangent plane
#   - M*(K*phi)   : quadratic normal correction encoding surface curvature
chart_eval &lt;- function(chart, xi) {
  q &lt;- quad_features(xi)          # 3-vector: [xi1^2, xi1*xi2, xi2^2]
  as.vector(
    chart$mean +
    chart$L %*% xi +              # 3x2 * 2x1 = 3x1 tangent contribution
    chart$M * as.numeric(chart$K %*% q)   # 3x1 * scalar normal correction
  )
}

# Jacobian of the inverse chart map at xi: a 3x2 matrix.
# J(xi) = L + M * (K * dQ(xi))
#   dQ(xi) is the 3x2 Jacobian of the monomial map (quad_jacobian).
#   K * dQ  is 1x3 * 3x2 = 1x2; then M * (K*dQ) is 3x1 * 1x2 = 3x2.
# This is the key object for geodesic integration: it maps tangent vectors
# in R^2 chart coordinates to ambient R^3 velocity vectors.
chart_jacobian &lt;- function(chart, xi) {
  dQ &lt;- quad_jacobian(xi)                   # 3x2 monomial Jacobian
  chart$L + chart$M %*% (chart$K %*% dQ)   # 3x2 total Jacobian J(xi)
}

# Project an ambient R^3 point onto this chart's tangent-plane coordinates (R^2).
# The linear projection xi = L^T * (x - mean) is exact for points on the tangent
# plane; for points on the actual surface it is a first-order approximation.
chart_project &lt;- function(chart, x) {
  as.vector(t(chart$L) %*% (x - chart$mean))
}

# Test whether tangent coords xi lie within the chart's ellipsoidal domain.
# The domain is {xi in R^2 : xi^T * A * xi &lt;= 1}, a 2D Mahalanobis ball.
# This is O(d^2) = O(4) for d=2.
chart_in_domain &lt;- function(chart, xi) {
  as.numeric(t(xi) %*% chart$ell_A %*% xi) &lt;= 1
}

# ===========================================================================
# PART 3: atlas_learn() — the main learning function
# ---------------------------------------------------------------------------
# Input : X (N x 3 matrix of surface points), k (number of charts)
# Output: an S3 object of class &quot;atlas&quot; containing k atlas_chart objects
#
# The algorithm runs four steps per chart:
#   (a) k-medoids partitioning
#   (b) local PCA to find the tangent plane and normal
#   (c) quadratic regression for the curvature coefficients K
#   (d) ellipsoidal domain construction
#
# d=2, D=3 specializations that are hard-coded here:
#   - SVD takes nv=3 (the full 3x3 right-singular-vector matrix)
#   - L = V[,1:2] is 3x2; M = V[,3] is 3x1 (one normal vector, not a matrix)
#   - nu (normal offset) is a scalar per point, not a vector
#   - K is fitted as a 1x3 row vector (one row per normal direction)
#   - ell_A is 2x2 (domain is a 2D ellipse)
# ===========================================================================

atlas_learn &lt;- function(X, k, ellipsoid_scale = 1.1) {
  # X              : N x 3 matrix of surface points
  # k              : number of charts
  # ellipsoid_scale: inflate domains by this factor so adjacent charts overlap

  message(&quot;Fitting k-medoids (k = &quot;, k, &quot;) ...&quot;)
  # PAM (Partitioning Around Medoids) is preferred over k-means for surfaces:
  # medoids are actual data points, making the partition robust to outliers.
  km &lt;- cluster::pam(X, k, diss = FALSE)

  charts &lt;- seq_len(k) |&gt;
    purrr::map(\(j) {

      # --- (a) Extract the cluster and center it ----------------------------
      idx &lt;- which(km$clustering == j)
      Xj  &lt;- X[idx, , drop = FALSE]   # N_j x 3 matrix of cluster points
      m   &lt;- colMeans(Xj)             # chart center (3-vector)
      Xc  &lt;- sweep(Xj, 2, m)         # centered cluster: N_j x 3

      # --- (b) Local PCA: tangent plane and normal -------------------------
      # SVD of the centered cluster reveals local geometry:
      #   - first two right singular vectors (v1, v2) span the tangent plane
      #   - third right singular vector (v3) is the surface normal
      # d=2, D=3: we always take nv=3 because D=3 (fully determined).
      sv  &lt;- svd(Xc, nu = 0, nv = 3)

      # L: 3x2 tangent basis (orthonormal columns). General form: R^{D x d}.
      L   &lt;- sv$v[, 1:2, drop = FALSE]

      # M: 3x1 unit normal. d=2, D=3 specialization: D-d=1, so there is
      # exactly ONE normal direction and M is a column vector, not a matrix.
      M   &lt;- sv$v[, 3, drop = FALSE]

      # Project each centered point into tangent / normal coordinates.
      # tau: N_j x 2 — tangent coordinates (2D because d=2)
      # nu:  N_j x 1 — normal offsets (scalar because D-d=1)
      tau &lt;- Xc %*% L    # N_j x 2
      nu  &lt;- Xc %*% M    # N_j x 1 (scalar per point; would be N_j x (D-d) in general)

      # --- (c) Quadratic curvature regression ------------------------------
      # Fit: nu ~ K * phi(tau)  where phi(tau) = [tau1^2, tau1*tau2, tau2^2]
      # K is 1x3 here (one output because D-d=1; would be (D-d)x3 in general).
      #
      # Design matrix Q: N_j x 3, each row is phi(tau_i)
      Q   &lt;- t(apply(tau, 1, quad_features))   # N_j x 3

      # Ridge-regularized least squares: K = nu^T * Q * (Q^T*Q + esp*I)^{-1}
      # The ridge penalty eps=1e-10 prevents singularity when clusters are
      # nearly collinear in tangent coordinates.
      K   &lt;- t(nu) %*% Q %*% solve(crossprod(Q) + 1e-10 * diag(3))  # 1 x 3

      # --- (d) Ellipsoidal domain ------------------------------------------
      # Domain: {xi in R^2 : xi^T A xi &lt;= 1}, a Mahalanobis ball.
      # A_raw = Cov(tau)^{-1}: inverse covariance of the tangent coordinates.
      # This adapts the domain shape to the data spread in each direction.
      A_raw &lt;- solve(cov(tau))

      # Scale A so the outermost point lands on the boundary, then inflate
      # by ellipsoid_scale (default 1.1) to ensure overlap with neighbors.
      qvals &lt;- apply(tau, 1, \(xi) as.numeric(t(xi) %*% A_raw %*% xi))
      ell_A &lt;- A_raw / (ellipsoid_scale * max(qvals))   # 2x2 positive-definite

      # Pack all chart data into an S3 object
      structure(
        list(mean = m, L = L, M = M, K = K, ell_A = ell_A, idx = idx,
             n_points = length(idx)),
        class = &quot;atlas_chart&quot;
      )
    })

  # Return the full atlas as an S3 object
  structure(
    list(
      charts        = charts,
      k             = k,
      clustering    = km$clustering,
      n_points      = nrow(X),
      ambient_dim   = ncol(X),   # D = 3
      intrinsic_dim = 2L         # d = 2 (hard-coded for surfaces in R^3)
    ),
    class = &quot;atlas&quot;
  )
}

# ===========================================================================
# PART 4: S3 print / summary methods
# ===========================================================================

print.atlas_chart &lt;- function(x, ...) {
  cat(sprintf(
    &quot;&lt;atlas_chart&gt;  %d points | mean [%s] | cond(ell_A) = %.1f\n&quot;,
    x$n_points,
    paste(round(x$mean, 3), collapse = &quot;, &quot;),
    kappa(x$ell_A)
  ))
  invisible(x)
}

# Returns a per-chart summary tibble
summary.atlas &lt;- function(object, ...) {
  purrr::imap_dfr(object$charts, \(ch, i)
    tibble::tibble(
      chart      = i,
      n_points   = ch$n_points,
      mean_norm  = round(sqrt(sum(ch$mean^2)), 4),
      cond_ell_A = round(kappa(ch$ell_A), 1)
    )
  )
}

print.atlas &lt;- function(x, ...) {
  cat(sprintf(
    &quot;&lt;atlas&gt;  k = %d | ambient R^%d | intrinsic R^%d | %d points\n\n&quot;,
    x$k, x$ambient_dim, x$intrinsic_dim, x$n_points
  ))
  print(summary(x))
  invisible(x)
}

# ===========================================================================
# PART 5: Chart lookup
# ===========================================================================

# Return the index of the nearest chart whose domain contains x.
# Falls back to the globally nearest chart center if none qualify.
# Checking only the 6 nearest charts (by Euclidean center distance) is
# sufficient in practice and avoids an O(k) domain test at every step.
find_chart &lt;- function(atlas, x) {
  dists      &lt;- map_dbl(atlas$charts, \(ch) sum((x - ch$mean)^2))
  candidates &lt;- order(dists)[seq_len(min(6L, atlas$k))]
  in_domain  &lt;- purrr::keep(
    candidates,
    \(i) chart_in_domain(atlas$charts[[i]],
                         chart_project(atlas$charts[[i]], x))
  )
  if (length(in_domain) &gt; 0L) in_domain[[1L]] else which.min(dists)
}

# ===========================================================================
# PART 6: Quasi-Euclidean retraction
# ---------------------------------------------------------------------------
# Advances one step from (chart_idx, xi) in the ambient direction tau_r3 (R^3).
#
# The pseudoinverse J^+ = (J^T J)^{-1} J^T is the key d=2, D=3 quantity:
#   - J is 3x2 (D x d)
#   - J^T J is 2x2 (d x d) — the pullback metric g; invertible for full-rank J
#   - J^+ is 2x3 (d x D) — pulls ambient vectors back to chart coordinates
#
# In general D &gt; d, J^+ is the minimum-norm left inverse of J.
# ===========================================================================

retract_step &lt;- function(atlas, chart_idx, xi, tau_r3) {
  chart  &lt;- atlas$charts[[chart_idx]]
  J      &lt;- chart_jacobian(chart, xi)        # 3x2 (D x d for d=2, D=3)
  Jp     &lt;- solve(crossprod(J)) %*% t(J)    # 2x3 pseudoinverse: (J^T J)^{-1} J^T
  xi_new &lt;- xi + as.vector(Jp %*% tau_r3)   # advance in chart coordinates

  if (chart_in_domain(chart, xi_new)) {
    # Still inside the same chart: evaluate and return
    return(list(
      chart_idx = chart_idx,
      xi        = xi_new,
      x         = chart_eval(chart, xi_new)
    ))
  }

  # Step crossed a chart boundary: find a new host chart and re-project
  x_cand &lt;- chart_eval(chart, xi_new)       # approximate ambient position
  ci_new &lt;- find_chart(atlas, x_cand)       # nearest chart that contains x_cand
  xi2    &lt;- chart_project(atlas$charts[[ci_new]], x_cand)   # re-project
  list(
    chart_idx = ci_new,
    xi        = xi2,
    x         = chart_eval(atlas$charts[[ci_new]], xi2)
  )
}

# ===========================================================================
# PART 7: Geodesic path integration
# ---------------------------------------------------------------------------
# Traces a geodesic from x_start in direction direction_r3 (ambient R^3).
# Returns a tibble with columns x, y, z, step, chart_idx.
#
# At each step the ambient velocity vector is re-projected into the current
# chart's tangent plane and pushed back to ambient.  This keeps the direction
# of motion consistent across chart boundaries — without it, the fixed tau
# vector drifts after every chart transition and the path wanders off the
# intended geodesic.
# ===========================================================================

atlas_geodesic &lt;- function(atlas, x_start, direction_r3,
                           n_steps = 100L, step_size = 0.02) {
  ci  &lt;- find_chart(atlas, x_start)
  ch  &lt;- atlas$charts[[ci]]
  xi  &lt;- chart_project(ch, x_start)

  # Project the initial direction onto the starting chart's tangent plane,
  # then normalize to unit speed.  This ensures tau_r3 is always in T_x M.
  J      &lt;- chart_jacobian(ch, xi)
  Jp     &lt;- solve(crossprod(J)) %*% t(J)          # 2x3 pseudoinverse
  tau_r3 &lt;- as.vector(J %*% (Jp %*% direction_r3))  # project onto tangent plane
  tau_r3 &lt;- tau_r3 / sqrt(sum(tau_r3^2)) * step_size  # unit-speed scaling

  steps     &lt;- vector(&quot;list&quot;, n_steps + 1L)
  chart_ids &lt;- integer(n_steps + 1L)
  steps[[1L]]     &lt;- chart_eval(ch, xi)
  chart_ids[[1L]] &lt;- ci

  for (i in seq_len(n_steps)) {
    res    &lt;- retract_step(atlas, ci, xi, tau_r3)
    ci     &lt;- res$chart_idx
    xi     &lt;- res$xi
    steps[[i + 1L]]     &lt;- res$x
    chart_ids[[i + 1L]] &lt;- ci

    # Re-project tau_r3 into the current chart's tangent plane and normalize.
    # This is the identity-transport approximation: it holds the direction
    # constant in the current chart's frame rather than applying the
    # Christoffel correction that true parallel transport would require.
    J_new  &lt;- chart_jacobian(atlas$charts[[ci]], xi)
    Jp_new &lt;- solve(crossprod(J_new)) %*% t(J_new)
    tau_r3 &lt;- as.vector(J_new %*% (Jp_new %*% tau_r3))
    tau_r3 &lt;- tau_r3 / sqrt(sum(tau_r3^2)) * step_size
    J      &lt;- J_new
  }

  do.call(rbind, steps) |&gt;
    as_tibble(.name_repair = &quot;minimal&quot;) |&gt;
    set_names(c(&quot;x&quot;, &quot;y&quot;, &quot;z&quot;)) |&gt;
    mutate(step = row_number(), chart_idx = chart_ids)
}

# ===========================================================================
# PART 8: Sphere-specific helpers
# ---------------------------------------------------------------------------
# These are ground-truth functions for S^2 used only for validation;
# they play no role in the Atlas-Learn algorithm itself.
# ===========================================================================

# Great-circle geodesic distance between two unit vectors (in radians).
# d(p, q) = arccos(p . q), clamped to [-1, 1] to avoid NaN from floating point.
sphere_dist &lt;- function(p, q) {
  acos(pmax(pmin(sum(p * q), 1.0), -1.0))
}

# Unit tangent vector at p pointing toward q along the great circle.
# Computed by projecting q onto the tangent plane at p and normalizing.
geodesic_direction_sphere &lt;- function(p, q) {
  v   &lt;- q - sum(p * q) * p   # remove radial component
  nrm &lt;- sqrt(sum(v^2))
  if (nrm &lt; 1e-12) return(NULL)
  v / nrm
}

# Run the Atlas geodesic from p toward q for the exact number of steps
# needed to cover the true great-circle distance, then measure endpoint error.
atlas_endpoint_error &lt;- function(atlas, p, q, step_size = 0.02) {
  d   &lt;- sphere_dist(p, q)
  dir &lt;- geodesic_direction_sphere(p, q)

  if (d &lt; 1e-6 || is.null(dir)) {
    return(tibble(true_dist = d, endpoint_error = 0, n_steps = 0L))
  }

  n_steps  &lt;- max(1L, round(d / step_size))
  path     &lt;- atlas_geodesic(atlas, p, dir,
                              n_steps = n_steps, step_size = step_size)

  endpoint &lt;- as.numeric(path[nrow(path), c(&quot;x&quot;, &quot;y&quot;, &quot;z&quot;)])
  # Re-project endpoint onto S^2 before measuring angle error.
  # This separates geodesic-direction error from on-manifold drift.
  endpoint &lt;- endpoint / sqrt(sum(endpoint^2))

  tibble(
    true_dist      = d,
    endpoint_error = sphere_dist(endpoint, q),
    n_steps        = as.integer(n_steps)
  )
}</pre>
</details>
</div>
</section>
<section id="construct-the-atlas" class="level1">
<h1>Construct the Atlas</h1>
<p>This section of code constructs the sphere point cloud by sampling 2,000 points uniformly from <img src="https://latex.codecogs.com/png.latex?S%5E2"/> using the standard parametrization <img src="https://latex.codecogs.com/png.latex?(%5Ctheta,%20%5Cphi)"/> with <img src="https://latex.codecogs.com/png.latex?%5Ccos%5Cphi"/> drawn uniformly on <img src="https://latex.codecogs.com/png.latex?%5B-1,1%5D"/>, which avoids the pole-crowding that arises from drawing <img src="https://latex.codecogs.com/png.latex?%5Cphi"/> uniformly.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>#| code-summary: &quot;Show the code&quot;

#| label: generate-sphere

set.seed(4471)
N     &lt;- 2000L
theta &lt;- runif(N, 0, 2 * pi)
phi   &lt;- acos(runif(N, -1, 1))   # uniform area measure on S^2

sphere_pts &lt;- tibble(
  x = sin(phi) * cos(theta),
  y = sin(phi) * sin(theta),
  z = cos(phi)
)

X_sphere &lt;- as.matrix(sphere_pts)</pre>
</details>
</div>
<p>This block of code fits an atlas with <img src="https://latex.codecogs.com/png.latex?k%20=%2025"/> charts to the sampled sphere. The fit is cached to <code>atlas_s2_cache.rds</code> to avoid unnecessarily repeating the this time consuming process during development sessions.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>#| label: learn-atlas

atlas_cache &lt;- &quot;atlas_s2_cache.rds&quot;

if (file.exists(atlas_cache)) {
  message(&quot;Loading cached atlas from &quot;, atlas_cache)
  atlas_s2 &lt;- readRDS(atlas_cache)
} else {
  message(&quot;Cache not found — fitting atlas (k = 25) ...&quot;)
  atlas_s2 &lt;- atlas_learn(X_sphere, k = 25L)
  saveRDS(atlas_s2, atlas_cache)
  message(&quot;Atlas saved to &quot;, atlas_cache)
}

# Attach chart labels to the point-cloud tibble for visualization
sphere_pts &lt;- sphere_pts |&gt;
  mutate(chart = factor(atlas_s2$clustering))</pre>
</details>
</div>
</section><section id="the-atlas-data-structure" class="level2">
<h2 class="anchored" data-anchor-id="the-atlas-data-structure">The atlas data structure</h2>
<p>In this section we provide an overview of the structure of the Atlas. The<code>atlas_learn()</code> function returns an S3 object of class <code>&quot;atlas&quot;</code> containing a nested list of 25 <code>&quot;atlas_chart&quot;</code> objects, one per cluster.</p>
<pre>atlas_s2
├── k              integer          number of charts (25)
├── n_points       integer          total data points (2000)
├── ambient_dim    integer          dimension of embedding space (3 = D)
├── intrinsic_dim  integer          dimension of the manifold (2 = d)
├── clustering     integer[2000]    chart label for every data point
└── charts         list[25]         one atlas_chart per cluster
    └── charts[[j]]
        ├── mean     numeric[3]     centroid of cluster j in R³
        ├── L        numeric[3×2]   orthonormal tangent basis (columns = v₁, v₂)
        ├── M        numeric[3×1]   unit surface normal = v₃  [For d=2: single vector]
        ├── K        numeric[1×3]   quadratic curvature coefficients  [for d=2: 1 row]
        ├── ell_A    numeric[2×2]   ellipsoidal domain matrix  [d=2 specialisation: 2×2]
        ├── idx      integer[n_j]   row indices into the N×3 data matrix
        └── n_points integer        cluster size n_j</pre>
<p>Here are the values for each field of the <img src="https://latex.codecogs.com/png.latex?k%20=%2025"/> charts.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>purrr::imap_dfr(atlas_s2$charts, function(ch, j) {
  tibble(
    Chart  = j,
    `n pts` = ch$n_points,
    `centre (x,y,z)` = paste(round(ch$mean, 2), collapse = &quot;, &quot;),
    # For the unit sphere, M should align with the radial direction (M·r̂ ≈ 1)
    `M · r̂`  = round(abs(sum(ch$M * ch$mean / sqrt(sum(ch$mean^2)))), 3),
    # Quadratic curvature coefficients
    k1 = round(ch$K[1], 3),
    k2 = round(ch$K[2], 3),
    k3 = round(ch$K[3], 3),
    # Semi-axes of the 2D ellipsoidal domain (1/sqrt of eigenvalues of ell_A)
    `ell axis a` = round(1 / sqrt(eigen(ch$ell_A, only.values=TRUE)$values[2]), 3),
    `ell axis b` = round(1 / sqrt(eigen(ch$ell_A, only.values=TRUE)$values[1]), 3)
  )
}) |&gt; print(n = 25)</pre>
</details>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 25 × 9
   Chart `n pts` `centre (x,y,z)`    `M · r̂`     k1     k2     k3 `ell axis a`
   &lt;int&gt;   &lt;int&gt; &lt;chr&gt;                 &lt;dbl&gt;  &lt;dbl&gt;  &lt;dbl&gt;  &lt;dbl&gt;        &lt;dbl&gt;
 1     1      82 0.91, 0.3, -0.16      0.999 -0.216 -0.078 -0.068        0.498
 2     2      70 0.35, -0.9, -0.06     1     -0.136  0     -0.163        0.486
 3     3      84 -0.64, 0.71, -0.13    1      0.182 -0.031  0.101        0.44 
 4     4      73 -0.04, 0.95, -0.12    1      0.162 -0.024  0.112        0.463
 5     5      81 -0.75, 0.4, 0.46      1      0.115 -0.056  0.184        0.431
 6     6      73 0.28, 0.6, -0.7       0.999 -0.203  0.016 -0.114        0.518
 7     7      78 -0.93, 0.07, -0.22    1      0.169 -0.087  0.055        0.46 
 8     8      86 -0.08, -0.3, 0.91     1     -0.237  0.051  0.099        0.53 
 9     9      73 -0.18, -0.87, -0.38   1      0.111 -0.036  0.127        0.421
10    10      85 0.87, 0.07, 0.4       1     -0.148 -0.002 -0.13         0.471
11    11      75 -0.36, 0.21, 0.87     1      0.2   -0.034  0.054        0.556
12    12      67 -0.79, -0.52, -0.21   1      0.178 -0.001  0.109        0.431
13    13      84 0.04, 0.05, -0.96     1      0.182  0.012  0.081        0.446
14    14      66 -0.39, -0.83, 0.31    1     -0.189 -0.006 -0.066        0.511
15    15      84 0.28, -0.55, -0.74    1      0.132 -0.066  0.105        0.431
16    16      87 -0.52, 0.43, -0.68    1     -0.251  0.009  0.026        0.511
17    17     100 0.83, -0.44, -0.2     1      0.144  0.082  0.133        0.526
18    18      68 0.07, -0.8, 0.55      1     -0.24   0.072 -0.088        0.474
19    19      91 0.59, 0.71, 0.24      0.999  0.229 -0.002 -0.003        0.537
20    20      93 -0.81, -0.26, 0.45    1      0.17   0.084  0.115        0.455
21    21      95 0.41, 0.27, 0.83      1      0.155 -0.087  0.056        0.532
22    22      66 -0.46, -0.32, -0.78   1     -0.179 -0.025 -0.072        0.525
23    23      70 -0.12, 0.78, 0.54     1      0.183  0.045  0.1          0.495
24    24      93 0.67, 0.07, -0.69     1      0.192 -0.001  0.116        0.453
25    25      76 0.62, -0.45, 0.58     1     -0.215 -0.039  0.028        0.512
# &#x2139; 1 more variable: `ell axis b` &lt;dbl&gt;</pre>
</div>
</div>
<p>Key things to notice:</p>
<ul>
<li><em><code>M · r̂</code></em> is uniformly <img src="https://latex.codecogs.com/png.latex?%5Capprox%201"/>, confirming the SVD-learned normal aligns with the sphere’s outward radial direction.</li>
<li><em>k1, k2, k3</em> encode local curvature. The Hessian <img src="https://latex.codecogs.com/png.latex?%5Cbegin%7Bpmatrix%7D2k_1%20&#038;%20k_2%20%5C%5C%20k_2%20&#038;%202k_3%5Cend%7Bpmatrix%7D"/> should have eigenvalues <img src="https://latex.codecogs.com/png.latex?%5Capprox%20%5Cpm%201"/> for a unit sphere (developed further in the curvature proof section).</li>
<li><em><code>axes a/b</code></em> semi-axes being nearly equal (near-circular domains) reflects the sphere’s isotropy.</li>
</ul>
<p>The following plot shows how the charts cover the sphere. Each point is colored by the chart it belongs to. With <img src="https://latex.codecogs.com/png.latex?k%20=%2025"/> charts, the sphere is tiled into roughly equal-area patches.</p>
<div class="cell">
<details class="code-fold">
<summary>Show the code</summary>
<pre>#| label: atlas-chart-plot

n_charts &lt;- length(unique(sphere_pts$chart))
chart_colors &lt;- colorRampPalette(RColorBrewer::brewer.pal(9, &quot;Set1&quot;))(n_charts)

plot_ly(
  data   = sphere_pts,
  x = ~x, y = ~y, z = ~z,
  type   = &quot;scatter3d&quot;,
  mode   = &quot;markers&quot;,
  color  = ~chart,
  colors = chart_colors,
  marker = list(size = 2.5, opacity = 0.6),
  text   = ~paste0(&quot;Chart &quot;, chart),
  hoverinfo = &quot;text&quot;
) |&gt;
  layout(
    title  = &quot;S² coloured by Atlas-Learn chart membership (k = 25)&quot;,
    scene  = list(
      xaxis = list(title = &quot;x&quot;),
      yaxis = list(title = &quot;y&quot;),
      zaxis = list(title = &quot;z&quot;),
      aspectmode = &quot;cube&quot;
    ),
    showlegend = FALSE
  )</pre>
</details>
<div class="cell-output-display">
<div class="plotly html-widget html-fill-item" id="htmlwidget-b25e2fe42113f49c5c85" style="width:100%;height:464px;"></div>
<script type="application/json" data-for="htmlwidget-b25e2fe42113f49c5c85">{"x":{"visdat":{"15e5137488d1f":["function () ","plotlyVisDat"]},"cur_data":"15e5137488d1f","attrs":{"15e5137488d1f":{"x":{},"y":{},"z":{},"mode":"markers","marker":{"size":2.5,"opacity":0.59999999999999998},"text":{},"hoverinfo":"text","color":{},"colors":["#E41A1C","#AA3B50","#705C83","#377EB8","#3E8E93","#459E6E","#4DAF4A","#658E67","#7E6E85","#984EA3","#BA5E6C","#DC6E36","#FF7F00","#FFA910","#FFD421","#FFFF33","#E1C62F","#C38E2B","#A65628","#C1645A","#DB728C","#F781BF","#D789B2","#B891A5","#999999"],"alpha_stroke":1,"sizes":[10,100],"spans":[1,20],"type":"scatter3d"}},"layout":{"margin":{"b":40,"l":60,"t":25,"r":10},"title":"S² coloured by Atlas-Learn chart membership (k = 25)","scene":{"xaxis":{"title":"x"},"yaxis":{"title":"y"},"zaxis":{"title":"z"},"aspectmode":"cube"},"showlegend":false,"hovermode":"closest"},"source":"A","config":{"modeBarButtonsToAdd":["hoverclosest","hovercompare"],"showSendToCloud":false},"data":[{"x":[0.85828767384000682,0.9956897805207271,0.99323115081949587,0.8676198323669565,0.97921027852975862,0.97106691843995907,0.97099414275410367,0.9264296214191039,0.92602836371821051,0.94631824868215364,0.83836565837582477,0.94559710052508428,0.99718662155994309,0.68628531865677778,0.91477144170169356,0.91237874278582864,0.75568331345719564,0.92614493733463188,0.93421786431192033,0.96380406691522136,0.91485148947180195,0.97227986280172096,0.80436992454472001,0.97506008197248228,0.91681537697953963,0.96962598816887402,0.89790344126080424,0.89999940077217899,0.90414486383917303,0.76528130584656462,0.93718361192964683,0.88591715120099523,0.99417155670331492,0.9299646682336784,0.92722896150263834,0.97901864850868292,0.90825719836036189,0.97579090889948694,0.99678531531469616,0.99431267307705939,0.95427991071964613,0.97566576838449459,0.89460957495896865,0.6759952351808477,0.71068212412284482,0.97895803877417142,0.95962452099787965,0.89688507496210923,0.97629297134894011,0.69693350868881077,0.95642167148182344,0.87517180110813952,0.93210489544591046,0.98204848419001833,0.91556261845095865,0.70392247313588108,0.98881379159875138,0.95881626498295125,0.86916214186023888,0.86234405361916466,0.98065112518938247,0.99366895666272659,0.93911410213545521,0.95327058002271003,0.82654787539156971,0.92822519140821025,0.92320251468450876,0.91933791339216109,0.9975388650668302,0.80527095047761243,0.92623314022636938,0.93326028006307804,0.8852262176292649,0.76286489759342901,0.82127884561860653,0.88077505178087911,0.806696310029414,0.82202542553712199,0.83966487399880796,0.97356441717513487,0.95170249834950316,0.7322740680649571],"y":[0.23192239371423373,0.019844078959082558,0.0079721947325498022,0.37903009879356697,-0.0080366849489469181,0.23751006450572096,0.2187894986419246,0.30494599438712727,0.34941322455810792,0.0420586889110173,0.48506679431793531,0.21973423322251232,0.049127659893130136,0.65998770821924413,0.08813475659545919,0.33139482008981286,0.56633416961669669,0.34267143015266521,0.32653129263233599,0.26064823914992397,0.36487358820267934,0.22886919343528656,0.58091815876658859,0.21881463355556519,0.37420855687473548,0.21193540580838574,0.13583714759075396,0.30121221302896312,0.39411376892148936,0.5311285365424655,0.046833171166568342,0.45076971123291409,-0.076961769141358785,0.31821185314788586,0.37181264332709119,0.19864517515247815,0.32883874808176228,0.20076487179974165,0.072154069815583199,0.071531059327439514,0.29809070636610496,0.16726556992549366,0.44643608905280741,0.64913906240558839,0.58457006266936007,0.13079865456786718,-0.022491580969319053,0.36414756947076776,0.19139922680828059,0.67634311889245224,0.26259931524496899,0.46061132233351709,0.19243756622582514,0.14639625624468192,0.16702800637157356,0.67675358559970755,-0.052413204281694513,0.15525172004178062,0.44106949578501053,0.48051702965035326,0.19407485679895559,-0.025474930892972297,0.13707262769885203,0.2929825735102185,0.49680575433104468,0.34714850081783838,0.37940836702330666,0.3487224384979315,0.053867690655229998,0.5481968282936146,0.24692473608161561,0.13664279062350754,0.20939695631873159,0.59832318888226133,0.56989042770673115,0.29750786452079331,0.58593157225335557,0.50066215949291482,0.41670486222776143,0.060674825415295931,0.29877070014317492,0.6667726103280055],"z":[-0.45777098229154944,-0.090598418843001169,-0.11588065046817052,-0.32182605657726526,-0.20268853474408383,-0.024859790224581933,0.096444440074265045,-0.22076253546401847,-0.14276508009061206,-0.32048843801021565,-0.24870309187099324,-0.23992288392037159,-0.056615499779582044,-0.30566106457263231,-0.39424037607386714,-0.24029669770970929,-0.32895035808905942,0.15758123621344575,-0.14357679802924386,0.056073309388011722,-0.17295669065788388,-0.047861891798674908,-0.12459180271252987,-0.037121323868632254,0.13934676349163064,-0.12210170691832896,-0.41870954073965538,-0.31507504079490883,-0.16491331765428172,-0.36365780699998135,-0.34567836439236999,-0.10935021098703145,-0.075497032608836931,0.18413835112005486,-0.044741604942828285,0.0454156389459969,-0.25871594343334431,-0.086750033777207108,0.034825642593205045,0.078902570996433496,-0.02217617584392418,-0.14176931092515588,-0.019197051413357265,-0.34878205182030791,-0.39141890639439209,-0.15662972349673512,-0.2803835007362066,-0.2509854775853454,-0.1010859538801015,-0.23841910576447847,0.12766826525330555,0.14802543120458722,-0.30683586327359064,-0.11894919443875539,-0.36585070285946131,-0.21563333738595253,-0.13964290730655188,-0.2378408573567867,-0.22364004794508219,0.15958106843754663,-0.025657759513705795,-0.10942135285586112,-0.31508062127977599,0.073732034303248054,-0.2645801431499421,-0.13373822346329678,-0.06120790727436537,-0.18223737785592661,-0.044883009977638604,-0.22587371198460446,-0.28481633495539416,-0.33219572156667704,-0.41536424774676556,-0.24504389334470025,-0.026943610515445488,-0.36840789718553413,-0.076975684612989426,-0.27131458092480887,-0.3483101450838148,-0.22020647395402188,0.070699528791010449,-0.13852427713572976],"mode":"markers","marker":{"color":"rgba(228,26,28,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(228,26,28,1)"}},"text":["Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1","Chart 1"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"1","textfont":{"color":"rgba(228,26,28,1)"},"error_y":{"color":"rgba(228,26,28,1)"},"error_x":{"color":"rgba(228,26,28,1)"},"line":{"color":"rgba(228,26,28,1)"},"frame":null},{"x":[0.21797452921762139,0.36199219674571237,0.42146103819334213,0.52043856035575786,0.00052536318122974328,0.60580312107566969,0.46317913142595757,0.3363960299072104,0.39966603026347591,0.3733707724485994,0.36360312896169938,0.60142617831602552,0.6000771514587977,0.5367245816472741,0.39476428842571826,0.2213757442891775,0.35906412981019242,0.59361775496227864,0.34296063821515982,0.19722638600895381,0.061002298897287879,0.44730934178708209,0.19891059587328144,0.2986357250669564,0.18370515059096026,0.19295244997773464,0.13393239085812658,0.52123196219193768,0.40377458297140112,0.5921075158949094,0.25419971100262573,0.5086327114235295,0.41080129494532813,0.202664973375456,0.058946790789073993,0.32545651181472579,0.30277740946958337,0.12591639674398231,0.23456280735478563,0.4160805834428169,0.43236083345479315,-0.0066324754135548853,0.37478763875601789,0.34462952211867032,0.60628428505063015,0.51158553719388633,0.19344131856935776,0.4103776691510056,-0.0090196028510151609,0.3813982929592889,0.54119755139737469,0.56933086470290961,0.34154771285449192,0.23087064304598023,0.47044983866678858,0.092909417717534179,0.64347422729976755,0.25270344698198943,0.57217991011057434,0.533096728259848,0.47899619874655014,0.34029377095598556,0.61312380670108646,0.34350768127425674,0.29365959012725146,0.41977286839554412,0.56431951087107757,0.022399319400934588,0.039410886272324801,0.33218886712152979],"y":[-0.96597843163876962,-0.92315542307204057,-0.90635960171050078,-0.82671073704175713,-0.99986634306586331,-0.79559410200354252,-0.86109165640248431,-0.92706301052524342,-0.85315532967413088,-0.9241437919174843,-0.92264295865323098,-0.78130990136668832,-0.79821889621009734,-0.84324689285250798,-0.9146981349993577,-0.95565652428285608,-0.93165383742558694,-0.8026350099867251,-0.90885081132054502,-0.96507919934857944,-0.99729388045755174,-0.82238357895411485,-0.89360022035879993,-0.90666180967711851,-0.97176101422702466,-0.95481756759329939,-0.98017798984967519,-0.78855862438054303,-0.85741650365914068,-0.80566236090097687,-0.95898994058048337,-0.85959343992495474,-0.90237892422703769,-0.9388202032703955,-0.99756862147167147,-0.87266870938102303,-0.94957987754622186,-0.97734877207428716,-0.97167468792542899,-0.87145060719563794,-0.90060945300656137,-0.99102875037061589,-0.91963327405745676,-0.92544933165371945,-0.77160943177288377,-0.79471964356257618,-0.91835678205778759,-0.90454912079914751,-0.99987205540862001,-0.90895624787634743,-0.83614249742776459,-0.81686447722654854,-0.92254343074652534,-0.97205241818801846,-0.81680705391797059,-0.99242093361820749,-0.75552643020391508,-0.96723259213887225,-0.80516081978298193,-0.83812330467734386,-0.86015660265373228,-0.93759292154548934,-0.78550423750255871,-0.93870854283548122,-0.87316856781994567,-0.8182779855371709,-0.77770412909642406,-0.99934725045811235,-0.99916901404456493,-0.90353298775443447],"z":[0.13918611360713837,-0.12940523307770496,0.029713055584579661,-0.21375935571268201,0.01634074654430152,0.0057099345140160431,-0.20972899533808231,0.16550494125112894,-0.3352507236413656,0.080947625916451313,0.12854079296812415,-0.16685739438980809,-0.052478624507784823,0.029349636286497269,-0.086535994894802556,-0.19419934973120689,-0.055624436121433828,-0.058266643434762941,0.23741988837718972,-0.17240618215873824,-0.041032128036022061,-0.35156735032796849,-0.4023844194598496,-0.29796151863411069,-0.14809776796028015,-0.22603708691895005,0.14599048905074599,-0.32633194373920549,0.3190658637322486,-0.017800276633352086,0.1253826175816358,-0.048906879965215783,0.13020972767844805,-0.27846639743074775,-0.037176892161369289,-0.36404310539364815,0.08138732379302388,0.17010126030072578,0.028785243630409359,0.25971289398148667,-0.044347749091684693,0.13348417961969966,0.11751113599166274,0.15739767160266641,-0.19245324237272141,0.32665107725188142,-0.34525537956505997,-0.11567651759833084,0.013210585806518892,-0.16832671081647263,0.089280089363455911,-0.092708102893084204,-0.1796072889119385,0.042577487882226837,-0.3339209277182818,-0.080427175853401353,0.12296638591215021,0.024537330493331119,-0.1559686018154024,-0.11557337269186967,0.17519492143765109,0.071551819797605262,-0.08403743524104354,-0.028787923045456319,-0.38902531703934068,-0.3926982013508678,-0.27698335191234941,-0.028343314770609088,0.010395355522632587,-0.27070038160309196],"mode":"markers","marker":{"color":"rgba(170,59,80,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(170,59,80,1)"}},"text":["Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2","Chart 2"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"2","textfont":{"color":"rgba(170,59,80,1)"},"error_y":{"color":"rgba(170,59,80,1)"},"error_x":{"color":"rgba(170,59,80,1)"},"line":{"color":"rgba(170,59,80,1)"},"frame":null},{"x":[-0.80784434337397881,-0.62836918113759199,-0.55580508785188998,-0.51037844630435547,-0.58119754743399166,-0.72894933354297631,-0.64116582787831033,-0.50173122555134653,-0.76929056854116551,-0.70713814566700584,-0.66186722291835987,-0.6592019554778078,-0.66066718286953963,-0.38548292643820492,-0.6555538113326248,-0.64094212189484123,-0.54553497782691351,-0.82667875514486822,-0.62641882568490181,-0.62474716527985608,-0.73603297401845125,-0.82883797518232893,-0.62109724889318485,-0.54003273837451415,-0.68396857624965446,-0.48155317981038637,-0.82710389287715247,-0.67649029365813695,-0.40007507345765464,-0.77510387806463865,-0.68385096764238762,-0.51525879933826979,-0.81105204224062144,-0.62023985455598574,-0.46897008183358196,-0.71885460467061013,-0.4365579232105144,-0.60179658050697982,-0.88233694523059447,-0.5264531500406634,-0.8390300232870368,-0.3970294321812699,-0.4539572454854105,-0.44290095729510859,-0.75141332893656032,-0.81054617055984746,-0.75051647505408536,-0.77613053345835115,-0.7952886891201506,-0.64410931149836481,-0.76587631258866906,-0.48325032204908386,-0.72292657380466452,-0.79473824122717462,-0.86069679639822294,-0.51708921657932228,-0.81413902875167665,-0.76222825293580598,-0.87867362728850296,-0.82765334597530038,-0.81321208755018937,-0.55624141549033168,-0.67070420655826057,-0.66597514492022825,-0.4976808668439649,-0.51352823760932631,-0.6206782043561011,-0.40565644560206293,-0.49805903048027533,-0.62303170220710924,-0.55678409697652598,-0.76946900792473061,-0.7907481121839951,-0.41661099411889929,-0.42849557231598673,-0.53722620428931411,-0.69833485140313822,-0.54793276903301957,-0.44656020134668467,-0.61837496098550593,-0.70422790366053445,-0.6281595044990933,-0.54270395341419464,-0.76159038867312301],"y":[0.53243092866635189,0.64989058202786121,0.79134437052927209,0.8311133200346742,0.7768664979426797,0.67765866776166417,0.76305359942056783,0.8443925331196529,0.63488698580757208,0.69254216335107122,0.72044725443260016,0.73468416650787838,0.72529052477615219,0.84862908597727049,0.71860683230310773,0.62390553166584972,0.81129714843497147,0.56264870935098232,0.69761999690568477,0.64251804269813728,0.66173249534435963,0.54169050560255771,0.78317359956933985,0.83630577574730169,0.68760299262150482,0.87565814909129847,0.54070803489083019,0.72019956001025442,0.87411763394465536,0.63165998263180079,0.66240911619737497,0.73022548591652015,0.55124978792302826,0.78410993083992486,0.88085022599095875,0.60735182435441937,0.84277384677139155,0.79859396066816291,0.46246452061806714,0.77336697125137521,0.51864221504178376,0.90926461135907177,0.7837533431860948,0.8643944970393499,0.63520332218817399,0.48034183584609197,0.65887640055159191,0.63056767603943464,0.60617421343275479,0.76482761500159624,0.52480339300252588,0.86578430902632086,0.67189178678314232,0.54402949534270617,0.4401749602202234,0.82424772515950095,0.57363717584913954,0.64618888613673697,0.47677383015490526,0.42634497336702243,0.48066165394449728,0.74656561166788216,0.68386708564180776,0.65860795297649277,0.85709931246984383,0.85100203083002324,0.78402064133459548,0.8312304389341747,0.82403440080073587,0.74227059795088013,0.82836984365230226,0.63703917830695389,0.61198132815992534,0.89288049270619674,0.84148300995555136,0.84026992826469693,0.71569172270836545,0.81988128490655776,0.88875938858496906,0.78536070553163206,0.70310657803779031,0.66493305075984255,0.82248233885695221,0.63059124843001468],"z":[-0.2527940329164266,-0.42754462175071251,0.25466603925451647,-0.22082683444023132,-0.24225576408207422,-0.09701339667662974,-0.081581772305071243,-0.18779517384245986,0.071488015819340986,0.14262185990810392,0.2070930534973742,-0.16028710920363648,-0.19357822230085731,-0.3622590065933764,0.23206339869648232,-0.44714101124554873,-0.2102106679230929,0.0053540319204331485,-0.34774386370554561,-0.44369082059711218,-0.14270797371864316,-0.13999645365402091,-0.029619594104588018,0.09464296558871868,-0.24369881255552162,-0.036460128147154949,-0.15340785961598163,-0.15386187424883238,-0.27542385086417198,-0.014820409938693041,-0.30587908858433355,-0.44864140404388309,-0.19575049448758366,0.021774737164378184,0.064575085882097635,-0.33818902820348745,-0.31488001346588135,0.0094107212498785644,-0.087224321439862113,-0.35320052178576594,-0.16443501086905601,-0.12492196168750518,-0.42385553708299978,-0.23803549213334921,0.17859100922942167,-0.33509196667000651,0.051057903096079965,0.0024085198529064625,0.0082899895496666553,-0.012724549975246123,-0.37150352960452432,0.12994866864755764,0.16105463588610305,-0.2691152840852738,-0.25582616962492455,0.2307258755899966,-0.09010012401267882,-0.038052777294069411,0.024887177161872536,-0.36499301716685295,-0.32810131832957257,-0.36501407530158758,-0.2871962334029376,-0.35030368342995627,-0.13302076281979672,0.1099285800009966,-0.0083785797469318017,-0.38012998504564149,-0.27000834885984648,-0.24671006761491288,-0.061602528207003938,-0.045809727627783972,0.014009891543537515,-0.17087921267375333,-0.32908644527196884,0.07303734030574556,0.010667397174984286,0.16602517664432528,0.10339601431041959,-0.028652571141719829,-0.09850989608094099,-0.4040787979029119,0.17027983209118244,-0.14944817591458562],"mode":"markers","marker":{"color":"rgba(112,92,131,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(112,92,131,1)"}},"text":["Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3","Chart 3"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"3","textfont":{"color":"rgba(112,92,131,1)"},"error_y":{"color":"rgba(112,92,131,1)"},"error_x":{"color":"rgba(112,92,131,1)"},"line":{"color":"rgba(112,92,131,1)"},"frame":null},{"x":[0.08732386984206339,-0.22580512344622267,-0.17822338343639457,-0.042613746794336452,-0.087844784514154714,-0.080907739303991616,-0.2658804553818539,-0.21377188528955529,-0.083603685115782603,0.33807161666225449,0.14622337401408486,-0.30305783956064475,-0.014183280315289799,-0.10606045509716983,-0.20991146392707799,0.24514214370888132,0.11848743779659664,0.10175852695064737,-0.22725921245366371,0.10182778511643226,-0.31944440930348289,0.01242793196379346,0.25223243845152066,-0.34542932434751261,-0.058017808470416619,-0.08467839794947285,-0.30431009167243761,0.11438269270370667,-0.22645762274270875,-0.059190243697837465,0.2849495467010974,-0.27373304379745461,-0.27859224075407352,-0.37783042132080713,0.2903014026649115,0.18785334848259816,-0.029158846856530671,0.041395339227985356,-0.034527271156466297,0.16757833495758853,0.064247304217548082,-0.12430587770405202,0.013352075369761799,-0.055810907833129586,0.027256214384781083,0.18132108130565724,0.053003547420055655,0.20899209516193168,-0.24172123450011901,-0.34168407155349656,-0.33880649357711407,-0.053206764941387076,0.26130898407587544,-0.091078606618733626,-0.21459455380615547,-0.12016687584483905,0.18853431916748442,0.31623478546965622,-0.067141129916429482,-0.33624160168904821,0.17121677738089619,0.093701285319188143,-0.26844558334839208,0.33390154271318656,0.10415852323346038,-0.20091029299146343,-0.1843841189040481,0.15885547088762506,-0.22908810431738316,0.21186861959585357,-0.33810934656423114,0.114392126403549,-0.15335590397314647],"y":[0.93692122508253894,0.97372017377953157,0.89659393794625264,0.88039144487222254,0.99489303255257411,0.88733319724070825,0.90448481883621223,0.9767238323036298,0.99524880781642511,0.89568484619088828,0.98441279873986343,0.94494454392303007,0.99490947739889413,0.96966247405381556,0.95715100342436155,0.96716242364202187,0.96228890063927697,0.99248540508294414,0.94218697616269409,0.96902750269642768,0.94760229579389643,0.95000094060597862,0.89929486964887206,0.92878912093324684,0.86402905868195479,0.97854060289408107,0.95013219743473853,0.99300071734820716,0.96648916390802242,0.9778652334001412,0.89744151727166177,0.83463879467888169,0.96011751530463685,0.91879575900335075,0.95308332122302175,0.92898467199195967,0.95684488249935395,0.99347214031116837,0.90896828396812135,0.95038200595303457,0.95700822727667578,0.99209130244105115,0.97974468663487368,0.87699942694339783,0.99908272974119916,0.98191511253542563,0.99099434712921275,0.96156332880910567,0.9547559780473216,0.93034802382988802,0.89540055451769907,0.99797249300912616,0.96524110895358439,0.88794486417199991,0.92583517980515562,0.99091708208754892,0.97795942489787535,0.94718561226412756,0.99609877613335118,0.93489332931884472,0.95321006056506719,0.99528224655305375,0.95388704128261359,0.90570990691956865,0.99427262913060099,0.94811181875113049,0.88284457990447218,0.95390279319540328,0.95556711359124336,0.88440362958823859,0.88955839819269256,0.99165981293379968,0.97049321509341158],"z":[-0.33845702791586507,-0.029682813212275366,-0.40540811046957975,-0.47232930501922948,0.049710638821125003,-0.45397547818720324,-0.33348882431164373,-0.017667384352534964,0.049902248196303976,-0.28888793382793654,-0.097724953666329342,-0.12343319971114389,-0.099769566208124147,0.22024047374725345,-0.19949720287695513,-0.067098253872245397,-0.24486894207075227,-0.06795530067756772,-0.2462457153014837,0.22498177969828245,-0.0022712047211824469,-0.31199961435049772,-0.35727795120328659,0.13427341775968668,-0.50008771196007729,0.18785062525421387,-0.068148187827318754,0.029430850408971355,0.12089516595005989,0.20068906387314203,-0.33675284544005973,-0.47796265874058019,0.023679531179368666,0.11427390761673455,0.085774579085409614,-0.31890217727050169,-0.28913255175575608,-0.1062992583028972,-0.41543293837457901,0.26209071790799515,-0.28285603551194066,0.017403918784111877,0.19980508275330081,-0.47723908862099051,0.033027229830622687,0.054455277509987396,-0.12296677567064768,0.17809623479843142,-0.1732393349520861,-0.13305844506248818,-0.28890830185264343,-0.034927687607705475,-0.0052169365808367929,-0.45084210718050588,-0.31109226495027537,-0.060359410010278232,0.089722760487347883,0.053244496230036013,-0.057265145238488767,-0.11364879272878159,-0.24914934393018479,0.025165826547890922,0.1342999674379827,-0.26115000341087563,-0.023936185985803576,-0.24641232378780831,-0.43195826699957246,-0.2546260012313723,0.18549968162551533,-0.41586284758523107,-0.30719362944364542,-0.059373873285949265,-0.18607763480395079],"mode":"markers","marker":{"color":"rgba(55,126,184,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(55,126,184,1)"}},"text":["Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4","Chart 4"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"4","textfont":{"color":"rgba(55,126,184,1)"},"error_y":{"color":"rgba(55,126,184,1)"},"error_x":{"color":"rgba(55,126,184,1)"},"line":{"color":"rgba(55,126,184,1)"},"frame":null},{"x":[-0.78606200508677559,-0.70607542753722252,-0.5742089148168884,-0.78243261271195208,-0.64320148767003982,-0.70978284732459085,-0.77790473654341763,-0.87684672040471789,-0.73478216532212781,-0.81068856934503231,-0.88868612224799859,-0.83904552222533779,-0.74936935286096351,-0.65460948801795116,-0.83298282535201451,-0.77627574151229739,-0.87551841747588777,-0.76565149794484044,-0.86681413008687824,-0.96404446385029796,-0.91044039274168254,-0.76936657542614595,-0.91606949670961413,-0.70888994014407802,-0.72775725458732288,-0.85890537837833147,-0.71556709502509785,-0.57799226608700449,-0.83432635088120388,-0.65097352461568236,-0.66581703786719515,-0.67988805956736986,-0.92429259235580219,-0.74349839540616891,-0.51916104135883567,-0.59359095171553511,-0.49061550651565883,-0.65544030750452231,-0.54605534485849327,-0.90040041522896885,-0.84223695696591738,-0.84723870107964627,-0.63893416799143332,-0.82588901809539617,-0.74397429147665972,-0.60099899418517211,-0.78574741370265766,-0.63974918057834584,-0.84797929138330785,-0.65230091531189693,-0.88923001451052086,-0.79709365989781411,-0.78356322163239889,-0.86437898739822316,-0.80956049779089845,-0.72505884253543285,-0.64080499748832032,-0.81597587600002752,-0.78463117919063607,-0.88273541811024747,-0.69722843395531664,-0.79074354644766176,-0.49266694426968538,-0.53027304978523082,-0.9380322360581278,-0.70693038063415348,-0.58314209437229347,-0.69435288691487329,-0.89210800143086222,-0.70743719824515361,-0.87456115034197957,-0.53771320476540874,-0.72053475806216316,-0.71945884317389253,-0.94159717924977027,-0.79557711180189339,-0.65066684738467806,-0.95932612775619974,-0.65364956861835888,-0.81463428147287642,-0.85489555415746432],"y":[0.27058863319515314,0.62747196155411444,0.59246261710753023,0.34221532463609078,0.58242242571179881,0.19756154567246942,0.57161907524608857,0.30976948536985549,0.09886189010511498,0.48802822087473574,0.39284588069664922,0.19623030205438455,0.60055873021206352,0.50855049206214231,0.47092307105230574,0.39354650084255588,0.40049291319557034,0.54835770700237507,0.077498223739922059,0.21325975070598716,0.34461400881871362,0.16454317459045081,0.34510699919356136,0.50343272153574958,0.3669737824840803,0.34792380178382115,0.35596515405870371,0.67814128997328083,0.3976128583531755,0.54550132510454175,0.32983736638040945,0.50967002176236853,0.35778377695755237,0.25318831661899116,0.55078295183186143,0.48914358869762409,0.58246548853726632,0.44472947130018686,0.68368899514383741,0.40376285536443807,0.45135110476525925,0.44720694864315991,0.63826388515521981,0.31981360510274831,0.25990153491353796,0.69490261882100868,0.18542160323741741,0.66603157978593652,0.098019426766741122,0.65080976843191451,0.37608559798443103,0.28338120389450905,0.36404274062916603,0.39205034141186557,0.15573789745837083,0.62208182183428495,0.3955424192755409,0.21835104463057114,0.37706352132112936,0.3622852988184157,0.12527094364706262,0.48843027074315087,0.6139109109379316,0.63903532899380189,0.30503864245274254,0.35217964172211774,0.38225272035998142,0.080505151034594477,0.3232115357363281,0.069854926636283746,0.1505980625102821,0.7127867811105677,0.21539551723166844,0.65239812197295466,0.25177609058927075,0.34320970833010161,0.27082578731690005,0.12704782667602046,0.38956284593359558,0.5092401742454371,0.49842551510239153],"z":[0.55577721772715449,0.32823227765038615,0.56504174135625362,0.52027673227712523,0.49706736393272882,0.67614920344203711,0.26099014095962059,0.36767199356108904,0.67105997959151864,0.32343855546787392,0.23645103117451075,0.50744091300293803,0.2788813090883196,0.55934141203761112,0.29046699265018111,0.49246027739718556,0.27032004576176411,0.33627609023824345,0.49257211573421966,0.15855141263455158,0.22877822490409014,0.61725247371941816,0.20424944628030076,0.49399448139593011,0.57939591072499763,0.3758065714500845,0.60104287834838033,0.45392657024785871,0.38184231705963623,0.52788424352183938,0.66924956766888499,0.52724633272737276,0.13294349424540991,0.61895542033016682,0.65353649714961648,0.63905268302187324,0.64809750765562069,0.61042092088609934,0.48414142383262526,0.16203286359086641,0.29482043441385042,0.28669239347800618,0.42939765006303793,0.46435610018670559,0.61559194745495915,0.39485511183738708,0.59010154288262129,0.38356605777516961,0.52088704472407699,0.38852305104956025,0.26044117240235215,0.53323239833116531,0.50348938489332795,0.31487377779558295,0.56600133189931512,0.29547230293974286,0.65796287870034587,0.53526273090392351,0.49211483774706721,0.29921153699979197,0.70581846218556166,0.36899934196844708,0.61675981990993034,0.55717532336711895,0.16445957170799386,0.61336688604205858,0.71681807702407241,0.7151174652390182,0.31571762217208749,0.70331564731895935,0.46093710837885743,0.45033267047256226,0.65911640366539359,0.23823447152972227,0.2236147406511009,0.49926361301913863,0.70942656183615327,0.25209567695856105,0.64883205108344555,0.27757058991119266,0.14396387524902826],"mode":"markers","marker":{"color":"rgba(62,142,147,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(62,142,147,1)"}},"text":["Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5","Chart 5"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"5","textfont":{"color":"rgba(62,142,147,1)"},"error_y":{"color":"rgba(62,142,147,1)"},"error_x":{"color":"rgba(62,142,147,1)"},"line":{"color":"rgba(62,142,147,1)"},"frame":null},{"x":[0.10383887873408955,0.48157435039227775,0.59716695818295529,0.3946098975765403,0.56322208008035779,0.32371651992007772,0.3557492862582981,0.45500329140759671,0.37677792193106174,0.25812356620538568,0.083017516189382509,0.31348591297353401,0.40847199419458546,0.29186162698274365,0.44771319862053049,0.58172550335051743,0.063066314369701906,0.47731304472792935,0.59581540905319474,-0.087914790692266237,0.25683242935922779,-0.0074670290486821894,0.30459792078265863,0.38034610004887864,0.22536434649498752,0.13797729965961258,0.65995481705982217,0.056275460327441885,0.62801209868326557,0.37335348034513216,0.07392935625097409,0.29299382888879139,0.011182126121374416,0.38338520985177921,0.26997194637829802,0.32893131508453671,0.20718723908590395,0.46450259101909597,-0.014410662100171962,0.26303850933143252,0.18590716732960294,0.38598614476298576,0.21534176496312118,0.16727746289273596,0.11543688545254863,0.13927292943250591,0.25272992215416312,0.53554202726984212,0.64591889941240199,-0.031689445099527008,-0.089641892151071809,0.15618769344635716,0.34174497103905155,0.085669820844694322,0.31304199402807009,0.49497804343152213,0.21592464543296155,0.21360066903064845,0.20824569052863598,-0.11317139235432057,-0.020461909454804587,0.44276648726020285,0.32529573207323648,-0.11362216449264692,0.048017163757659416,0.08436451153718609,0.51466321524971459,0.44104438832082543,0.17818114426183548,0.2423128254687304,0.49566329711967089,0.33623625635568322,0.36044110939803242],"y":[0.79566664474821058,0.36968591384765032,0.72437600924591172,0.59075820799322765,0.64235552366518478,0.67640313309353695,0.64701185128535232,0.40445594131760804,0.73841295990137579,0.32817361269906614,0.5335999146470144,0.57956954817484485,0.51947132863647105,0.62049582168027406,0.52115405604725851,0.51053125018907297,0.62422781125507509,0.74677556023876024,0.63771530071907301,0.76692723372210192,0.44747952068178332,0.44729418143489152,0.34399038783991459,0.33426996169202822,0.47357073825385665,0.49863075207487895,0.51330806938335916,0.55594615795830016,0.56585567380492685,0.39415057262907099,0.55470978046024189,0.33826403391546828,0.79370810650121848,0.40824477519794122,0.77320936958355058,0.86917653564626252,0.70481041158553326,0.61364428260185011,0.46789389462924541,0.49283155432150916,0.72588546895562134,0.67524185760789357,0.77852751778356444,0.8753845648755324,0.59278650943645239,0.60728576599993833,0.64483841612279391,0.71942309292938078,0.57694706894127079,0.72141195858623153,0.76534812375263095,0.61912665922104826,0.5961160998204954,0.48429578874321627,0.87190560889849711,0.8030067901915332,0.71221180454250732,0.67238930481027059,0.7212029197800337,0.65581528291657343,0.62740493782695972,0.4239624684612448,0.40485822479359673,0.5331852477027802,0.72897021864327438,0.63543428917713773,0.65496985537325414,0.60082425545500495,0.60870520601933809,0.47771664898133775,0.56550518297807939,0.68811547856973287,0.39963630250923127],"z":[-0.5967680267058314,-0.79461844312027097,-0.34448660537600517,-0.70376684237271558,-0.51976943900808681,-0.66157873021438718,-0.67440203856676806,-0.79333939542993914,-0.55927157821133722,-0.90866622282192111,-0.84165267366915941,-0.75221387995406996,-0.75053325621411204,-0.72787480102851998,-0.72660260228440166,-0.6332087186165154,-0.77869267342612147,-0.46313984924927343,-0.48818356543779368,-0.63568357517942786,-0.85662079229950916,-0.89435572270303976,-0.88819520361721516,-0.86232269881293189,-0.85143506340682518,-0.85576260602101684,-0.54861139738932263,-0.82931112498044979,-0.53423605300486066,-0.83979313215240836,-0.82875298475846637,-0.89427739521488547,-0.60819602245464921,-0.8284636288881303,-0.57381392363458883,-0.36923751141875966,-0.67846571886911999,-0.6384967011399566,-0.88366715237498283,-0.82941413158550847,-0.66221507918089617,-0.62854047585278761,-0.5895106685347854,-0.45356379263103019,-0.79704358708113443,-0.78218095703050483,-0.72132586501538754,-0.44229532033205043,-0.49992084875702858,-0.69178072037175287,-0.63734337734058477,-0.76960222469642769,-0.72653697105124604,-0.87069987412542105,-0.37654391396790732,-0.33192895539104927,-0.66793030546978116,-0.70870824530720711,-0.66068152757361531,-0.74639034736901511,-0.77842427650466561,-0.79007193539291631,-0.8545598308555783,-0.83833376131951809,-0.68285926245152961,-0.76753234025090944,-0.55329581908881664,-0.66670087864622463,-0.77312964759767056,-0.84443549066782009,-0.65918266354128729,-0.64299476519227028,-0.84283630223944783],"mode":"markers","marker":{"color":"rgba(69,158,110,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(69,158,110,1)"}},"text":["Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6","Chart 6"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"6","textfont":{"color":"rgba(69,158,110,1)"},"error_y":{"color":"rgba(69,158,110,1)"},"error_x":{"color":"rgba(69,158,110,1)"},"line":{"color":"rgba(69,158,110,1)"},"frame":null},{"x":[-0.96151498933112001,-0.96821556546481113,-0.93286677210069457,-0.99435675779007482,-0.97611525409026634,-0.91089686297545269,-0.93360386308169696,-0.94233388818847563,-0.82922298810545558,-0.98539703575972559,-0.96680049300150994,-0.99459923036494513,-0.97556626685028802,-0.99702390396625284,-0.92874068231965656,-0.98537669111829373,-0.83816099614763684,-0.81535412604555557,-0.85546200880880874,-0.86403961215384606,-0.89439619061838294,-0.92130422957288149,-0.98869201491662895,-0.93145918896136204,-0.95211221270650348,-0.99472582258103737,-0.93516660453611977,-0.92051082406766471,-0.90617235437086674,-0.85404136466526714,-0.98406559394060511,-0.88710960109845816,-0.97190658089637416,-0.98406595336721614,-0.95785657492616127,-0.868253712765928,-0.97718346366816144,-0.99250789536720374,-0.85001889754844639,-0.95339924488057259,-0.84612542232573573,-0.86018049845040878,-0.95880956630541181,-0.92041939357256275,-0.95704428239561112,-0.94166057543510162,-0.99515626366903109,-0.90805499110628596,-0.98969401533468382,-0.94725855608325271,-0.91482030366812361,-0.90231044987858089,-0.99000343436024463,-0.97061798986010472,-0.98772901506752064,-0.986024090228801,-0.96245772727102408,-0.86990462395919255,-0.98970297955887054,-0.99386752566525172,-0.94464794742694602,-0.96060900672671723,-0.92550561796938846,-0.82895030819349158,-0.99257402010006079,-0.97995700597573132,-0.95032169647236464,-0.92176810385388064,-0.87920808020281549,-0.86230315285126835,-0.90060058567542867,-0.99409878071902347,-0.84446801945679495,-0.94351332820369294,-0.97438361201036749,-0.98966117167207779,-0.82537095970672003,-0.93661480190636859],"y":[0.27162400616401577,-0.22317517922687763,-0.12589278447234084,-0.018725063153571666,0.11691441774757072,-0.065329154755329522,0.1751947558628579,0.3289217798892326,0.067887302523594525,-0.15682804355721952,0.00024270386576435648,0.077932540049628532,0.21292367324581896,0.06947833016365354,0.098438197093208904,0.055887786192222302,0.39588891084737049,-0.059880080954923393,-0.18054404272297828,0.17092893240357671,0.3909278960289338,-0.092734343754216159,-0.01674173205897456,0.029575002062003638,-0.22311354516953641,-0.10224854359834699,0.32505660831587868,0.3380899251849192,0.36471195297337888,0.028717842944080935,-0.17309232012478382,-0.17141804257948143,0.22349775283118323,0.057576820023631306,-0.13705457414557398,0.31802188338420556,0.1467512226521682,-0.10057214480501227,0.039883640944804059,0.089913955192236064,0.021049628511153162,0.17904156061075302,0.28221398884690985,0.30129111620847415,0.28983419701724966,-0.22909979416420059,0.084467317940897901,-0.12699132559798776,-0.068819915781534263,0.24021339211675713,-0.25095042722226957,0.040040394590689199,0.040697339794647631,0.22530641347518815,-0.068074490942309412,0.14723029204284852,0.016759549182991627,0.24219902415249422,-0.087200376671180801,0.095741206221265668,-0.054316840073821038,-0.26038877051654435,-0.19460402918395836,-0.056823557446794043,-0.11841883812321291,0.12395147521269179,0.30117098803684361,0.32501622618406162,-0.083521437410480556,0.37071780071999078,0.34200792322532003,0.012763901301040024,-0.0014497385773381303,0.01280829327838864,0.02565159607649484,0.12275013903393742,0.22654980851328871,0.24722424967942838],"z":[0.04134397860616458,-0.11292235460132349,-0.33750643301755195,0.10442226892337213,0.18311206856742498,-0.4074297565966844,-0.31255499413236965,-0.061784349847584803,-0.55477973120287061,-0.066314754541963283,-0.25553228333592409,-0.068548451177775735,-0.0541661181487142,0.0334080313332379,-0.35742225218564277,-0.16096375975757823,-0.37517744442448009,-0.57585764303803422,-0.48537470074370498,-0.47351752733811736,-0.21732656145468343,-0.37762263976037502,-0.1490228641778229,-0.36264183232560759,-0.20905186049640173,-0.008110069204121783,-0.14071824029088009,-0.19584439042955631,-0.21409543557092536,-0.51941181439906359,0.040668852161616038,-0.42854685895144945,-0.073799407109618048,0.16822339082136756,-0.25244172709062701,-0.38078546710312366,0.15354659548029306,-0.069378104060888277,-0.5252401060424744,-0.28800236200913776,-0.53256800770759583,-0.47752866894006729,-0.032240968663245419,-0.2491019936278463,-0.0078980866819618953,-0.24655353371053923,-0.050291978288441778,-0.39913573674857616,-0.12557696923613548,-0.2121290978975594,-0.31642960524186492,-0.42922327388077991,0.13504416495561614,0.084485133644193397,-0.14056050824001434,0.077972652856260635,-0.27091371454298485,-0.42965751234442001,-0.11350817838683724,-0.05532596912235023,-0.32355824252590537,-0.097098014317452741,-0.32491325447335839,-0.5564283151179551,-0.02781714219599957,-0.15594966569915417,-0.078642921987921027,-0.2114427001215517,-0.46906004007905705,-0.34496606327593327,-0.2682334161363541,0.10772509919479485,-0.53560401638969779,-0.33108691778033955,-0.2234246456064283,-0.07418334484100339,-0.51714404486119736,-0.24825970921665433],"mode":"markers","marker":{"color":"rgba(77,175,74,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(77,175,74,1)"}},"text":["Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7","Chart 7"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"7","textfont":{"color":"rgba(77,175,74,1)"},"error_y":{"color":"rgba(77,175,74,1)"},"error_x":{"color":"rgba(77,175,74,1)"},"line":{"color":"rgba(77,175,74,1)"},"frame":null},{"x":[-0.3599167746767547,-0.23355958536654514,0.14838658894708182,-0.34488478508343873,-0.095756391840087857,-0.16834286316500499,-0.12050563202449165,0.20749764914986099,-0.032598720229986479,-0.3959433364716563,-0.17726337601225781,-0.069911664919915018,-0.30791339571233778,0.28931482908012368,-0.39368246328020717,0.15133592023670925,0.12168890466985878,-0.45288152308937624,0.16633869623673295,0.15637142509223448,-0.36528643382003334,-0.18581958682082866,-0.31965238385326133,0.15308092597573927,0.35937144215511346,-0.32699386470267833,-0.0092401412146512867,0.0044803837193941112,0.10222786988058062,-0.25121580266043153,-0.03264138936532629,-0.16663578125930351,0.27317361753423341,-0.31753947202769078,0.0023642690427844873,-0.28061015384287269,0.0092601691491279968,-0.18484390022355673,-0.042365543631822468,-0.14151097936349402,-0.13170915286232976,0.14716296860819089,0.0037470914567929365,0.092742457560282932,0.27392651688820052,0.29025488771289626,0.10905509447055499,-0.25390902082361039,-0.43123789786464739,0.24805404438416145,0.15865256297110544,0.097405979812016638,0.28781131174899288,-0.15506905728213893,-0.31517398051698459,0.16922277042500858,0.22969626221659872,-0.37885658863285471,-0.13931158154996781,-0.43040735181693779,0.1325684705028442,-0.047089625987290169,-0.45630367399585792,0.22019340543473195,-0.4221426489767281,-0.2795757569935553,-0.1012850637303867,-0.21067834601110458,-0.3072983759803472,-0.18009092909333441,0.34288181441548365,-0.18672429255576717,-0.11824492223498019,-0.050854560064148242,-0.035562547241419981,-0.39644703748016685,-0.03307012600721633,-0.07151453185685662,-0.061798749480164297,-0.17150504436407449,-0.12889001881111672,-0.10585102515971527,-0.1521792554212871,0.057959258413511475,-0.44501204316174714,0.016807322328133031],"y":[-0.556525241795089,-0.43138219209115564,-0.43602970859656226,-0.18769298880622631,-0.10781275395894056,-0.22579168218538853,-0.23272488706660507,-0.23459887649892805,-0.026001289673893615,-0.51831074573436808,-0.51345398394777853,-0.21487743605059176,-0.36087131960946855,-0.24411208858593994,-0.36192972274416846,-0.34523766690847685,-0.2477248748386624,-0.37534481001988357,-0.084059659195420536,-0.13825933756376294,-0.52445660991799603,-0.47993635597247819,-0.39635360649493545,-0.060510600290541144,-0.21610219080697796,-0.28609678663445681,-0.45358145898950264,-0.06765812038320658,-0.38319291186873594,-0.28928094053081904,-0.15277136266319005,-0.60216571903369043,-0.13311691474871445,-0.23341261201793181,0.029925670809208695,-0.56140966658517855,-0.47808491755822852,-0.07314924956552496,-0.43980562023065117,-0.33201658424840502,-0.15261576829200549,-0.09097381232907463,0.06191994981402018,-0.52529563695942449,-0.37121290344764507,-0.40005750069485796,-0.1945952723170836,-0.15119778630806766,-0.46838912006173777,-0.26854539748947504,-0.50220629097603442,-0.45992243273240957,-0.22700220943163843,-0.58515619056895107,-0.30881990792111419,-0.25553188431898477,-0.19094626742275222,-0.46913685168503899,-0.42588318016161014,-0.25568723993854769,-0.44264926800803872,-0.24246239066761635,-0.48398142871239747,-0.31134802304140435,-0.17683871070047771,-0.33780184173148015,-0.19090351819072396,-0.50407641349355803,-0.086471257577074051,-0.11523192025640851,-0.22419633747639098,-0.13296321754476376,-0.56218753371197083,-0.12970352730519435,-0.49070751628211506,-0.36693867902757382,-0.28040494827817442,-0.41765087935847645,-0.092176188011882834,-0.20456653760362628,-0.24200529360427336,-0.27135350397862867,-0.11546976125852684,0.077206417656396603,-0.4181448854705837,-0.40709829921909629],"z":[0.74882546067237854,0.8714122585952282,0.88761450722813606,0.91968789650127292,0.98954894952476025,0.95952008664608002,0.96504793642088771,0.94968841876834631,0.99913024995476007,0.758012430742383,0.83960865996778011,0.974135538097471,0.88031882373616099,0.92558425758033991,0.84499762952327728,0.92623344389721751,0.96115773776546121,0.80871169129386544,0.98247921699658036,0.97797358501702547,0.76909757871180773,0.85739849274978042,0.86065450217574835,0.98635931452736259,0.90782818291336298,0.9006795440800488,0.89116692030802369,0.99769850401207805,0.91799382073804736,0.92369213374331594,0.98772235494107008,0.78078739950433373,0.95270985178649426,0.91906867874786258,0.99954933067783713,0.77850955538451672,0.8782648011110723,0.98004179494455457,0.89709318196401,0.9325983221642673,0.97946981899440289,0.98491970542818308,0.99807408498600125,0.84585065487772226,0.88722333358600736,0.86931357765570283,0.97480237297713757,0.95533734280616045,0.77113261353224516,0.93078061891719699,0.85006952984258533,0.88260038010776043,0.93039488699287176,0.79595591593533754,0.89738265331834555,0.95187557488679886,0.95434749964624643,0.79773322585970163,0.89398870244622231,0.86566364532336593,0.8868411504663527,0.96901731472462416,0.74669199390336871,0.92443348746746778,0.88911397149786353,0.8987365085631609,0.97636938840150833,0.83756886515766382,0.9476763317361474,0.97687709657475352,0.91223246138542891,0.97337290970608592,0.81851286813616753,0.99024785216897726,0.87059832224622369,0.84153773076832294,0.95931195747107267,0.90578883560374379,0.99382315576076508,0.96371082356199622,0.96167603740468621,0.95664143562316895,0.98158453963696957,0.99532903777435422,0.79190854029729962,0.91322970204055309],"mode":"markers","marker":{"color":"rgba(101,142,103,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(101,142,103,1)"}},"text":["Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8","Chart 8"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"8","textfont":{"color":"rgba(101,142,103,1)"},"error_y":{"color":"rgba(101,142,103,1)"},"error_x":{"color":"rgba(101,142,103,1)"},"line":{"color":"rgba(101,142,103,1)"},"frame":null},{"x":[-0.11666413169093415,-0.24736557465378331,0.030757363451849231,-0.25228658282535188,-0.38645888434283659,0.021208717367138273,0.15597462920028832,-0.0016349018419069391,0.075664266627518129,0.019411760692241813,-0.030813963531450033,-0.21909619683109754,0.038198010733356089,-0.39364540681873433,-0.018781844282762652,-0.47312473626085766,-0.068251094509597932,0.019853074203819154,-0.08776932026676286,-0.49509568045389307,-0.11408548239839141,-0.32939047163583357,0.092016857369616789,0.021156115110809177,-0.43031437049947535,-0.19975080819936095,-0.14923425012464203,-0.29884475787151449,-0.33490250732233645,-0.12320749282861912,-0.2962621653580933,-0.48927772408781312,-0.34913676269450222,-0.031617452760191324,-0.050321477802836065,-0.27737357597871004,0.073238017755678758,-0.23667646414253049,-0.16582932898264208,-0.2155728718950351,-0.01051805888434857,-0.27730857644860962,-0.52005162255756932,-0.25072657793765912,0.069401571152157449,-0.32433192249842224,-0.47318745487737601,-0.19761894925305928,0.0081793984227139117,-0.3794082064759326,-0.25782409874269951,-0.35949872357537355,-0.49264870200527588,-0.29565930733920087,-0.13149692313074979,0.096332912479825422,-0.064176546343506544,-0.064890143992612045,-0.23493375469512567,-0.16849223099461147,-0.33420858310287527,-0.44468834946670061,0.017448640144894459,0.11995459952293092,-0.094326122145092409,-0.23166376342155712,0.16782005558833019,-0.30112067250404889,0.089169434544269691,-0.22249803764833118,-0.31193775205589191,-0.15177499622365534,-0.41376288296004715],"y":[-0.99263651204301051,-0.96301951530592955,-0.92987631588994957,-0.85500237305940929,-0.74735915699960243,-0.84244921541247386,-0.88624737234834261,-0.82848551244867363,-0.86631826428012138,-0.96500420044802926,-0.901487644495928,-0.96941490364197225,-0.82502406765850733,-0.90450207303889851,-0.96510234443581044,-0.86165734312512909,-0.82358113419364209,-0.78790589948768308,-0.99459552950618457,-0.78771474313915701,-0.92838011867805204,-0.83269097711301487,-0.94803492179444449,-0.88093150892876171,-0.72382087297531195,-0.83410950814260953,-0.84458120402042369,-0.85886487463114702,-0.92968088400039217,-0.97501463741883276,-0.75089669924657898,-0.79183116033940337,-0.80277771561688416,-0.84599661612926591,-0.96618439797490141,-0.91232760568735727,-0.81588406611687925,-0.94040338375940147,-0.87294122724706091,-0.92355390938653481,-0.82443982166198515,-0.79572973723286766,-0.83520303400728046,-0.66116818968133528,-0.97166323662906873,-0.91242607660599662,-0.73929149175869913,-0.90112794936393081,-0.98269242243717148,-0.90309701150789812,-0.9320545673790821,-0.80710540113339591,-0.76771670325117958,-0.75878225659739074,-0.68102761423143066,-0.95501701476853973,-0.99682001390223174,-0.98386939233538384,-0.74745337551170343,-0.96555686066998569,-0.79230203366352747,-0.84375654167353897,-0.9449162756473799,-0.8483334747552751,-0.83007447417536917,-0.76512580585780565,-0.87079977102593864,-0.72736711774360541,-0.92917444726668408,-0.97028047020430663,-0.78008874460655997,-0.96705198894272892,-0.89167956906958712],"z":[-0.032591952010989064,-0.10678804060444227,-0.36658426281064738,-0.45312517276033748,-0.54046629974618543,-0.53835816122591484,-0.43616225197911246,-0.5600081095471976,-0.49372824886813743,-0.26151496451348061,-0.43170652817934768,-0.11059656925499425,-0.56380510795861472,-0.16407100157812241,-0.2611985970288514,-0.18357452703639851,-0.56307717366144061,-0.61547554703429341,-0.055464214645326082,-0.36658662091940641,-0.35368751455098385,-0.44511532643809909,-0.30456967186182732,-0.47277065832167853,-0.53936340846121311,-0.51416042540222406,-0.51420980971306574,-0.41598429996520275,-0.15340718533843756,-0.18484147405251858,-0.59023967711254932,-0.36552800470963104,-0.48337507201358659,-0.53224999969825137,-0.25289416359737515,-0.30120132677257055,-0.57355852657929052,-0.24418379785493016,-0.45877472404390579,-0.3171380036510526,-0.56585188070312153,-0.53843675460666407,-0.17883568396791813,-0.70710169570520531,-0.22595127020031205,-0.24957455554977051,-0.47910512704402219,-0.38589528342708945,-0.18506404384970659,-0.20115963974967591,-0.25456043984740967,-0.46833912841975672,-0.40976617718115443,-0.58037493145093333,-0.7203540434129535,-0.28046830743551243,-0.047235905658453724,-0.16670419322326777,-0.62138521252199996,-0.19826829526573414,-0.51045284839347005,-0.30054478906095017,-0.32684671785682445,-0.5156948803924023,-0.54961709398776304,-0.60076151834800828,-0.46210841555148358,-0.61665502237156022,-0.35872504580765957,-0.095133760478347443,-0.54236186202615499,-0.20438884804025295,-0.18359690299257625],"mode":"markers","marker":{"color":"rgba(126,110,133,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(126,110,133,1)"}},"text":["Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9","Chart 9"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"9","textfont":{"color":"rgba(126,110,133,1)"},"error_y":{"color":"rgba(126,110,133,1)"},"error_x":{"color":"rgba(126,110,133,1)"},"line":{"color":"rgba(126,110,133,1)"},"frame":null},{"x":[0.84716923297785385,0.79908672476228393,0.87096631232768162,0.87668948786450562,0.99092515073210752,0.95416080693026406,0.74155913049247812,0.96883391349909687,0.97228506023008898,0.74246271398978247,0.98239383442301298,0.81228898823990681,0.86431939404191294,0.74919246998335198,0.81421224086814681,0.96281242335731332,0.75977236333202158,0.76566870385383534,0.71520112768477184,0.73826197632774415,0.91892792084897357,0.78425365329711427,0.89118283594815606,0.93049356452704268,0.90968211316748315,0.99058315013439346,0.91607930102393564,0.99360309671474989,0.83854528694508579,0.99380726877914216,0.87411771531185867,0.94771041518387111,0.79778121873473729,0.73372174156811854,0.93077683377537912,0.6766549856796249,0.74937958491184931,0.91197618472331476,0.83519773466632219,0.82063935808704691,0.990771708420357,0.79251572861579633,0.88978196204969784,0.72624182473681398,0.87542460120840726,0.88185341045436438,0.74761089207018427,0.7224046240256411,0.67341352345443461,0.94915341961282607,0.95339170889456371,0.9614711056346178,0.80329206389622998,0.78025803184513243,0.89259477445381952,0.99392376040276942,0.81060917882646277,0.95716652556586257,0.92789554122400297,0.91563584759137095,0.85726524322851561,0.93102177064279723,0.83407385098415254,0.98019016235885692,0.86140704479514307,0.98795126832390412,0.95757391465460284,0.90336177297209708,0.94820871534420792,0.93751755683592986,0.87235753060561849,0.98988362829485443,0.93602585065514898,0.82231098097576549,0.86619402659815647,0.86337869930232747,0.95185263093767414,0.85346470311599032,0.83197139984331892,0.87324433613405916,0.97587670654147807,0.84250503863527526,0.87299602830207124,0.92148686239063871,0.90517310514931748],"y":[0.20348552728913719,0.11551452450478612,0.1801121955195378,0.26789491443951458,-0.10079121190468354,0.20880154363464523,0.14387572754208594,-0.14075755989185637,0.10665601681200254,0.021108368832741899,0.10425635634198128,0.13060962595135808,0.38364113691549606,-0.111853776241542,-0.011615326959860875,-0.03967896210238215,0.33304407085353049,0.1939727613972532,0.0028872700636324152,-0.10050194582534831,0.013609739778835315,0.18602121152870521,-0.11526357174548979,-0.074989462646009808,-0.16910079427492788,-0.013166134642053675,0.12131768881178502,-0.070152470134958983,0.25871335197013312,0.016213231022295094,0.13298795614860726,0.045058030078184264,0.018730732814802,0.21270999956026568,0.29927952773572875,-0.040436412837378456,0.34517419485573247,-0.11174113350683203,0.34266353159315982,-0.19590662601689732,-0.013351959791644497,0.2332147547013981,0.37956210147664154,0.30315493579995439,0.14854601201838574,-0.084268558380788811,0.32392657255253454,0.16001257587330167,-0.036642439277641055,0.24546491832928627,-0.073881277603268097,0.060128915759437303,0.41021277815803198,-0.133882830075682,0.092509525615633331,-0.070934970158045049,0.20892975526005539,-0.18779794888234683,0.30243566323821092,-0.28986146469574531,0.37947032810706294,-0.27407152339684882,-0.066924723348565571,-0.033057782011154883,0.42254641225114781,-0.11871479230654898,0.054114584506339769,0.12685346852495044,0.026936383961783387,-0.25913681144851131,-0.24450556615506486,0.029766354467221218,-0.19250970836467851,-0.15425525091682857,0.11495315840254045,0.019250502238511661,-0.13317958843772756,-0.030688816601782899,0.16671935394207232,0.365353610125079,-0.17123768932147937,0.089619342125485002,-0.01771554298066276,0.26519774458882095,0.30738928011226424],"z":[0.49081353982910525,0.59001423791050922,0.457140328362584,0.39955957839265471,0.088929619640111909,0.21442730678245425,0.65527843777090322,0.20383365126326694,0.20805349247530111,0.66955474391579628,0.15502569545060413,0.56844324711710226,0.32522524986416107,0.6528394715860486,0.5804511271417141,0.26724112220108509,0.55841526016592979,0.61329112481325865,0.69891273463144898,0.66698471736162901,0.39419062808156019,0.59189721755683422,0.43875672295689577,0.35855028498917829,0.37931962031871086,0.13627793500199925,0.38220509234815842,0.0884958594106139,0.47948848083615297,0.10992835694924007,0.46715353289619094,0.31593471299856896,0.60265602683648467,0.64529594918712985,0.20996725931763652,0.73518904158845544,0.56505310628563166,0.39473200729116803,0.43014700757339591,0.53681620489805937,0.13488197419792408,0.5634977356530726,0.25341797713190317,0.61698451917618513,0.45996287884190684,0.46393250860273844,0.57978403707966208,0.67270166845992219,0.73835733765736222,0.19711610767990367,0.29255051910877239,0.26824956014752399,0.43179548019543301,0.61096054827794444,0.44126698980107909,0.084165245294571048,0.54704763647168875,0.22037280397489681,0.21804250543937098,0.27855219598859549,0.34799220226705085,0.24100469425320625,0.54757820675149571,0.19528038473799822,0.28183759981766349,0.09929294791072614,0.28306149458512664,0.40968976635485882,0.31650381255894905,0.2321829958818854,0.42333127325400705,0.13872406631708159,0.29461096227169031,0.54773165704682469,0.48630615836009383,0.50418909126892697,0.27611549431458116,0.52024628501385461,0.52917695231735706,0.32242994429543625,0.13543377490714203,0.53118135640397668,0.48740547196939599,0.28378181532025348,0.29355319822207104],"mode":"markers","marker":{"color":"rgba(152,78,163,1)","size":2.5,"opacity":0.59999999999999998,"line":{"color":"rgba(152,78,163,1)"}},"text":["Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10","Chart 10"],"hoverinfo":["text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text","text"],"type":"scatter3d","name":"10","textfont":{"color":"rgba(152,78,163,1)"},"error_y":{"color":"rgba(152,78,163,1)"},"error_x":{"color":"rgba(152,78,163,1)"},"line":{"color":"rgba(152,78,163,1)"},"frame":null},{"x":[-0.43343551692761551,-0.25584032143929408,-0.46046991654698372,-0.48736120982820236,-0.44478553611461891,-0.32815496870302358,-0.2927038774873737,-0.36845748713261772,-0.59419658454966184,-0.55840461841908973,-0.36615140626316495,-0.2488610887689709,-0.35164371219886714,-0.23616986920844246,-0.50529703131325598,-0.23232335052098588,-0.47385608910889682,-0.39850722374158165,-0.074819803231724102,-0.42989176283810721,-0.16125777125512622,-0.22257832926485729,-0.5746968426929201,-0.24830216137928637,-0.31997290336427447,-0.23683470580659052,0.024401121555230377,-0.66539055093268096,-0.50339331249769725,-0.5337917345692833,-0.0040464281713065266,-0.47591601528086536,-0.23606156672585585,-0.57999395092040817,-0.44298587539708745,-0.25220431842993807,-0.22321731043278617,-0.53113212255888564,-0.40917053696236566,-0.24372337815684345,-0.49182878978269678,-0.070023709127922676,-0.3310468297763895,-0.12536406197127778,-0.22904732922278973,0.028735737119957621,-0.61212087255394465,-0.63917659203900923,-0.27141144926598215,-0.35867081360051839,-0.19124417902655552,-0.4157352354128952,-0.54108434304575781,-0.40660572577874726,-0.035624267722908325,-0.42451874433456271,-0.23351294827871394,-0.072345901703402382,-0.31749456526117065,-0.41883867283685466,-0.61299926427757656,-0.31046277003504186,-0.60881095745536384,-0.52425484883688778,-0.36486495887822218,-0.49531759871295106,-0.38629481023937173,-0.18058582365040213,-0.094124690624267829,-0.23121608829214288,-0.52163656487861398,-0.23199200232692568,-0.35009183747862643,-0.64764232469899841,-0.56825710491790571],"y":[0.55256336873831524,0.28149457688669366,0.023868387956185861,-0.025836044589537387,0.30753158842652317,-0.0079764383192590018,0.55662235341292221,0.57027665145339601,-0.13299528959368914,0.12983696570294043,0.22437310445236086,0.36832698986151752,-0.06495826328160173,0.32940749091147331,0.082891743998979542,0.38362224454768729,0.28180464124275983,0.095726328616079062,0.49983971145464101,0.32094858611784072,0.33289752121421773,0.11899172951820104,0.31985687914039623,0.33435564750456565,0.45266550874257189,0.25902327361061261,0.19644173427582348,0.051616294666287595,-0.13552486238529582,0.35673926582441989,0.2144312359544796,-0.11611644232167417,0.056309243400912583,-0.072183855502467517,0.11101007003278095,0.41645935089714964,0.51918866941708264,0.10744416285333271,0.31573561649788845,0.40805464293262034,0.2431066764073461,0.30458299269395778,0.2512115119173407,0.26945048808695399,0.38041085273986497,0.17248299165881317,0.21909766146795664,0.15260346233654229,0.037782523860005884,0.30134673009201135,0.039990426087143406,0.42322058139475599,0.19760380349600301,-0.11306219958511016,0.40429097281563198,0.51097111647597349,0.5363069334088667,0.36833029174338799,0.048421529100044172,0.5137205674177785,0.0527841476673701,-0.0065310662857745805,-0.037678435787612305,-0.15929408843269796,0.18698711478638291,-0.089368989725173556,-0.056489402216473077,0.20725795784599269,0.50548912671503654,0.43778304626588538,-0.10980388407701726,0.039009048202379222,0.29651523785174827,0.17877665822246136,0.25899661606138669],"z":[0.71190404985100031,0.92482783971354365,0.88735435763373971,0.8728181654587388,0.84118377836421132,0.94459022488445044,0.77749340562149882,0.73418228048831224,0.79324817797169089,0.81934525351971388,0.90310013713315129,0.8957697176374495,0.93387746717780828,0.91417421633377671,0.85895510297268629,0.89378959173336625,0.83429404348134995,0.91215594206005335,0.86288021178916097,0.8439105860888958,0.9290722100995481,0.96762588620185852,0.75326961698010564,0.90914923837408423,0.83229278400540352,0.93638467835262418,0.98021182930096984,0.74470878392457962,0.85336286807432771,0.76668342901393771,0.97673070570454001,0.87179178604856133,0.970105255022645,0.81141635915264487,0.88962929276749492,0.87347271898761392,0.82499524718150496,0.84044893970713019,0.85609023598954082,0.87982402974739671,0.83606434287503362,0.94990835385397077,0.90956075815483928,0.95481950463727117,0.8960049687884748,0.98459325358271599,0.75980540411546826,0.75376751553267241,0.96172153251245618,0.88348480220884085,0.98072750028222799,0.80501462938264012,0.8174230670556426,0.90658078668639064,0.91393638774752617,0.74745725886896253,0.81107741687446833,0.92687586368992925,0.94...</script></div></div></section>
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://rworks.dev/posts/atlas-learn-sphere/"> R Works</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/the-atlas-learn-approach-to-the-manifold-hypothesis/">The Atlas-Learn Approach to the Manifold Hypothesis</a>]]></content:encoded>
					
		
		<enclosure url="https://rworks.dev/posts/atlas-learn-sphere/geo.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401347</post-id>	</item>
		<item>
		<title>More crochet/programming thoughts</title>
		<link>https://www.r-bloggers.com/2026/05/more-crochet-programming-thoughts/</link>
		
		<dc:creator><![CDATA[Maëlle&#039;s R blog on Maëlle Salmon&#039;s personal website]]></dc:creator>
		<pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://masalmon.eu/2026/05/19/crochet-again/</guid>

					<description><![CDATA[<p>Here I am again, writing about crochet and programming!<br />
I’ve continued creating my tons of shitty stitches cute creatures.<br />
New Git analogy! The crochet lifeline<br />
First Git/crochet analogy.<br />
I read a great crochet book about creating your own amigurumi pa...</p>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/more-crochet-programming-thoughts/">More crochet/programming thoughts</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://masalmon.eu/2026/05/19/crochet-again/"> Maëlle&#039;s R blog on Maëlle Salmon&#039;s personal website</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<p>Here I am again, writing about crochet and programming!
I’ve continued creating <a href="https://masalmon.eu/2026/01/26/amigurumi/#the-importance-of-practice" rel="nofollow" target="_blank"><del>my tons of shitty stitches</del></a> cute creatures.</p>
<h2 id="new-git-analogy-the-crochet-lifeline">New Git analogy! The crochet lifeline</h2>
<p><em><a href="https://masalmon.eu/2026/02/15/stitch-markers-git-commits/" rel="nofollow" target="_blank">First Git/crochet analogy</a></em>.</p>
<p>I read a great crochet book about creating your own amigurumi patterns: <a href="https://www.editions-eyrolles.com/livre/creer-ses-propres-modeles-d-amigurumis-au-crochet" rel="nofollow" target="_blank"><em>Créer ses propres modèles d’amigurumis au crochet</em></a> by Clotilde Massot and Lise Grandjonc.
One of its authors (Clotilde Massot) is a software developer who, among other things, published an octocat pattern that I bought and <a href="https://bsky.app/profile/masalmon.eu/post/3mjw2zxta2k2z" rel="nofollow" target="_blank">used</a>…
Anyway in the book they explain that when you create a pattern, you will probably have to undo your work several times.
Undoing a round is easy if you have a stitch marker in the first stitch of the current/last round: you undo until you hit that stitch marker.
But what about undoing several rounds?
In that case, you’ll be better off if instead of using a stitch marker at the beginning of the current round, you use contrasting yarn stuck <em>under the first stitch of each round</em>.
That yarn creates what the authors of the book call your <strong>lifeline</strong>!</p>
<p>Now, if that’s not a great analogy for commits and the ability to reset…</p>
<h2 id="communities-of-practice">Communities of practice</h2>
<p>If you’ve had the opportunity to attend or watch the wonderful <a href="https://yabellini.netlify.app/talk/2025_user2025/" rel="nofollow" target="_blank">useR! 2025 keynote talk of my rOpenSci colleague Yanina Bellini Saibene</a>, you’ve heard of communities of practice.
In her talk, Yani quoted Etienne Wenger who defined communities of practice as <em>“groups of people who share a passion for something that they know how to do, and who interact regularly in order to learn how to do it better“</em>.
Yani mentioned her swimming team and English conversation club.
Well, I found a community of practice for crochet: a stitch club at a local café!
Participants meet up to crochet side by side, talking a lot about crochet: comparing yarns, exchanging tips, etc.
The first time I went, we even started with a round of introductions where we said what each of us would work on during that meeting, which reminded me of <a href="https://ropensci.org/coworking/" rel="nofollow" target="_blank">rOpenSci coworking sessions</a> where participants do exactly that.</p>
<h2 id="usefulness-of-seeing-others-work">Usefulness of seeing others’ work</h2>
<p>I’ve worked on some patterns by Yan Schenkel a.k.a. <a href="https://picapauyan.com/" rel="nofollow" target="_blank">Pica Pau</a>.
One cool aspect of the patterns is that they include a link to a gallery where anyone can upload pics of their take on each creature.
So you can look at them, maybe seeing a crucial (to you) angle that’s absent from the pattern, noticing whether some “flaw” of your own project is present in others’ projects, comparing variations and picking what you prefer before you start, etc.
For instance I stared at many pics of <a href="https://www.amigurumi.com/forum/Animal-Friends-of-Pica-Pau-3/Alberto-Seagull/" rel="nofollow" target="_blank">Alberto Seagull</a> before making the legs for mine.</p>
<p>The usefulness of seeing others’ crocheted animals reminds me of how reading and reviewing open-source, or our colleagues’, code helps us (and LLMs, I suppose) learn how to do, or not to do, some things, how it help us refine our taste!
Ironically, I do publish most of my code, but I haven’t had interest in doing that for my crocheting yet. <img src="https://s.w.org/images/core/emoji/13.0.0/72x72/1f638.png" alt="😸" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>In summary, I keep finding excuses to talk about crochet. My pink octocat approves!</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://masalmon.eu/2026/05/19/crochet-again/"> Maëlle&#039;s R blog on Maëlle Salmon&#039;s personal website</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/more-crochet-programming-thoughts/">More crochet/programming thoughts</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401301</post-id>	</item>
		<item>
		<title>Querying Neo4j Aura from R with neo2R</title>
		<link>https://www.r-bloggers.com/2026/05/querying-neo4j-aura-from-r-with-neo2r/</link>
		
		<dc:creator><![CDATA[Patrice Godard]]></dc:creator>
		<pubDate>Mon, 18 May 2026 22:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://patzaw.github.io/posts/neo2R-Aura.html</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>1 Introduction<br />
Graph databases excel at storing and traversing highly connected data used for recommendation engines, fraud detection, knowledge graphs, and social networks. Neo4j is one of the most widely used graph databases, and with Neo...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/querying-neo4j-aura-from-r-with-neo2r/">Querying Neo4j Aura from R with neo2R</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://patzaw.github.io/posts/neo2R-Aura.html"> Patrice Godard</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 




<p><img src="https://i1.wp.com/patzaw.github.io/posts/images/neo2R-Aura.png?w=450&#038;ssl=1" class="img-fluid" alt="Illustration of neo2R used to connect to Aura, generated with Gemini"  data-recalc-dims="1"></p>
<script src="https://patzaw.github.io/site_libs/htmlwidgets-1.6.4/htmlwidgets.js"></script>
<link rel="stylesheet" href="https://patzaw.github.io/site_libs/vis-9.1.0/vis-network.min.css">
<script src="https://patzaw.github.io/site_libs/vis-9.1.0/vis-network.min.js"></script>
<script src="https://patzaw.github.io/site_libs/visNetwork-binding-2.1.4/visNetwork.js"></script>
<section id="introduction" class="level2" data-number="1">
<h2 data-number="1" class="anchored" data-anchor-id="introduction"><span class="header-section-number">1</span> Introduction</h2>
<p>Graph databases excel at storing and traversing highly connected data used for recommendation engines, fraud detection, knowledge graphs, and social networks. <strong>Neo4j</strong> is one of the most widely used graph databases, and with <strong>Neo4j Aura</strong>, its managed cloud service now makes it easy to spin up a production-grade instance without any infrastructure overhead.</p>
<p>On the R side, the <a href="https://cran.r-project.org/package=neo2R" rel="nofollow" target="_blank">neo2R</a> package has long been available for querying self-hosted Neo4j instances from R. Version <strong>3.0.0</strong> brings two important changes:</p>
<ol type="1">
<li><strong>Unified connection model</strong> — a single <code>startGraph()</code> call handles both a self-hosted Neo4j instance (<code>http://localhost:7474</code>) and a <em>cloud</em> Neo4j Aura instance (<code>https://&lt;id&gt;.databases.neo4j.io</code>).</li>
<li><strong>httr2 backend</strong> — the internal HTTP layer migrated from the deprecated <code>httr</code> package to <a href="https://httr2.r-lib.org/" rel="nofollow" target="_blank"><code>httr2</code></a>.</li>
</ol>
<p>In this post, we’ll connect to the <strong>free Neo4j Aura demo database</strong> preloaded with the classic Movie Recommendations dataset, explore the graph with Cypher queries, and finish with an interactive network visualization built with <a href="https://datastorm-open.github.io/visNetwork/" rel="nofollow" target="_blank">visNetwork</a>.</p>
<hr>
</section>
<section id="prerequisites" class="level2" data-number="2">
<h2 data-number="2" class="anchored" data-anchor-id="prerequisites"><span class="header-section-number">2</span> Prerequisites</h2>
<div class="cell">
<pre>install.packages(c(&quot;neo2R&quot;, &quot;dplyr&quot;, &quot;visNetwork&quot;))</pre>
</div>
<div class="cell">
<pre>library(neo2R)
library(dplyr)</pre>
<div class="cell-output cell-output-stderr">
<pre>
Attaching package: 'dplyr'</pre>
</div>
<div class="cell-output cell-output-stderr">
<pre>The following objects are masked from 'package:stats':

    filter, lag</pre>
</div>
<div class="cell-output cell-output-stderr">
<pre>The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union</pre>
</div>
<pre>library(visNetwork)</pre>
</div>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>neo2R 3.0.0 requires R ≥ 4.1 and httr2 ≥ 1.0.0. Check your versions with <code>packageVersion(&quot;neo2R&quot;)</code> and <code>packageVersion(&quot;httr2&quot;)</code>.</p>
</div>
</div>
<hr>
</section>
<section id="connecting-to-neo4j-aura" class="level2" data-number="3">
<h2 data-number="3" class="anchored" data-anchor-id="connecting-to-neo4j-aura"><span class="header-section-number">3</span> Connecting to Neo4j Aura</h2>
<section id="create-and-connect-to-an-aura-instance" class="level3" data-number="3.1">
<h3 data-number="3.1" class="anchored" data-anchor-id="create-and-connect-to-an-aura-instance"><span class="header-section-number">3.1</span> Create and Connect to an Aura Instance</h3>
<p>Neo4j provides a <strong>free Aura Free</strong> tier (up to 200 k nodes / 400 k relationships).</p>
<p>Create a free instance at https://console.neo4j.io and get your connection details.</p>
<p>Connect to your instance with <code>startGraph()</code>.</p>
<div class="cell">
<pre>my_aura &lt;- startGraph(
  url = &quot;https://&lt;INSTANCEID&gt;.databases.neo4j.io&quot;,
  database = &quot;INSTANCEID&quot;,
  username = &quot;INSTANCEID&quot;,
  password = &quot;INSTANCEPASSWORD&quot;
  ## api = &quot;v2&quot; is set automatically for *.databases.neo4j.io URLs
)</pre>
</div>
</section>
<section id="the-movie-recommendations-dataset" class="level3" data-number="3.2">
<h3 data-number="3.2" class="anchored" data-anchor-id="the-movie-recommendations-dataset"><span class="header-section-number">3.2</span> The Movie Recommendations Dataset</h3>
<p>Neo4j provides <a href="https://neo4j.com/docs/getting-started/appendix/example-data/" rel="nofollow" target="_blank">example datasets</a>, and most of them are available as a one-click templates in the <a href="https://console.neo4j.io/" rel="nofollow" target="_blank">Neo4j Aura console</a>.</p>
<p>The <a href="https://github.com/neo4j-graph-examples/recommendations" rel="nofollow" target="_blank">Movie Recommendations dataset</a> is a graph example using a dataset of movie reviews for generating personalized, real-time recommendations. This dataset is also available on a demo server that can be accessed as follows.</p>
<div class="cell">
<pre>graph &lt;- startGraph(
  url = &quot;https://demo.neo4jlabs.com:7473&quot;,
  database = &quot;recommendations&quot;,
  username = &quot;recommendations&quot;,
  password = &quot;recommendations&quot;
)</pre>
</div>
</section>
</section>
<section id="exploring-the-schema" class="level2" data-number="4">
<h2 data-number="4" class="anchored" data-anchor-id="exploring-the-schema"><span class="header-section-number">4</span> Exploring the schema</h2>
<p>The Movie database contains the following node labels and relationship types:</p>
<div class="cell">
<div class="cell-output-display">
<table class="caption-top table table-sm table-striped small">
<thead>
<tr class="header">
<th style="text-align: left;">Node label</th>
<th style="text-align: left;">Key properties</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">Movie</td>
<td style="text-align: left;">title, released, imdbId</td>
</tr>
<tr class="even">
<td style="text-align: left;">Genre</td>
<td style="text-align: left;">name</td>
</tr>
<tr class="odd">
<td style="text-align: left;">Actor</td>
<td style="text-align: left;">name, born, imdbId</td>
</tr>
<tr class="even">
<td style="text-align: left;">Director</td>
<td style="text-align: left;">name, born, imdbId</td>
</tr>
<tr class="odd">
<td style="text-align: left;">User</td>
<td style="text-align: left;">name</td>
</tr>
</tbody>
</table>
</div>
</div>
<p><br></p>
<div class="cell">
<div class="cell-output-display">
<table class="caption-top table table-sm table-striped small">
<thead>
<tr class="header">
<th style="text-align: left;">Relationship type</th>
<th style="text-align: left;">Key properties</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td style="text-align: left;">IN_GENRE</td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: left;">ACTED_IN</td>
<td style="text-align: left;">role</td>
</tr>
<tr class="odd">
<td style="text-align: left;">DIRECTED</td>
<td style="text-align: left;"></td>
</tr>
<tr class="even">
<td style="text-align: left;">RATED</td>
<td style="text-align: left;">rating || timestamp</td>
</tr>
</tbody>
</table>
</div>
</div>
<p><br></p>
<p>Let’s count the number of these different concepts:</p>
<div class="cell">
<pre>## Node types and counts
cypher(
  graph,
  &quot;
  MATCH (n)
  RETURN labels(n) AS label, count(n) AS n
  ORDER BY n DESC
&quot;
) |&gt;
  as_tibble() |&gt;
  ## filtering out technical nodes
  filter(label %notin% c(&quot;_Bloom_Perspective_&quot;, &quot;_Bloom_Scene_&quot;, &quot;&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 6 × 2
  label                           n
  &lt;chr&gt;                       &lt;int&gt;
1 Actor || Person             14956
2 Movie                        9125
3 Director || Person           3604
4 User                          671
5 Actor || Director || Person   487
6 Genre                          20</pre>
</div>
<pre>## Relationship types and counts
cypher(
  graph,
  &quot;
  MATCH ()-[r]-&gt;()
  RETURN type(r) AS type, count(r) AS n
  ORDER BY n DESC
&quot;
) |&gt;
  as_tibble() |&gt;
  ## filtering out technical relationships
  filter(type %notin% c(&quot;_Bloom_HAS_SCENE_&quot;))</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 4 × 2
  type          n
  &lt;chr&gt;     &lt;int&gt;
1 RATED    100004
2 ACTED_IN  35910
3 IN_GENRE  20340
4 DIRECTED  10007</pre>
</div>
</div>
</section>
<section id="querying-with-cypher" class="level2" data-number="5">
<h2 data-number="5" class="anchored" data-anchor-id="querying-with-cypher"><span class="header-section-number">5</span> Querying with Cypher</h2>
<section id="top-prolific-actors" class="level3" data-number="5.1">
<h3 data-number="5.1" class="anchored" data-anchor-id="top-prolific-actors"><span class="header-section-number">5.1</span> Top prolific actors</h3>
<div class="cell">
<pre>cypher(
  graph,
  &quot;
  MATCH (p:Person)-[:ACTED_IN]-&gt;(m:Movie)
  RETURN p.name AS actor, count(m) AS movies
  ORDER BY movies DESC
  LIMIT 10
&quot;
) |&gt;
  as_tibble()</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 10 × 2
   actor             movies
   &lt;chr&gt;              &lt;int&gt;
 1 Robert De Niro        56
 2 Bruce Willis          49
 3 Samuel L. Jackson     45
 4 Nicolas Cage          45
 5 Michael Caine         40
 6 Clint Eastwood        40
 7 Tom Hanks             38
 8 John Cusack           38
 9 Morgan Freeman        38
10 Gene Hackman          38</pre>
</div>
</div>
</section>
<section id="movies-and-their-directors" class="level3" data-number="5.2">
<h3 data-number="5.2" class="anchored" data-anchor-id="movies-and-their-directors"><span class="header-section-number">5.2</span> Movies and their directors</h3>
<div class="cell">
<pre>cypher(
  graph,
  &quot;
  MATCH (d:Person)-[:DIRECTED]-&gt;(m:Movie)
  RETURN m.title AS movie, m.released as released, d.name AS director
  ORDER BY m.released IS NOT NULL DESC, m.released DESC
  LIMIT 10
&quot;
) |&gt;
  as_tibble()</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 10 × 3
   movie         released   director            
   &lt;chr&gt;         &lt;chr&gt;      &lt;chr&gt;               
 1 Solace        2016-09-02 &quot;Afonso Poyart&quot;     
 2 Ben-hur       2016-08-12 &quot;Timur Bekmambetov&quot; 
 3 Rustom        2016-08-12 &quot;Tinu Suresh Desai&quot; 
 4 Mohenjo Daro  2016-08-12 &quot;Ashutosh Gowariker&quot;
 5 Suicide Squad 2016-08-05 &quot;David Ayer&quot;        
 6 Shin Godzilla 2016-07-29 &quot;Hideaki Anno&quot;      
 7 Shin Godzilla 2016-07-29 &quot; Shinji Higuchi&quot;   
 8 Jason Bourne  2016-07-29 &quot;Paul Greengrass&quot;   
 9 Star Trek 3   2016-07-22 &quot;Justin Lin&quot;        
10 Ghostbusters  2016-07-15 &quot;Paul Feig&quot;         </pre>
</div>
</div>
</section>
<section id="parameterised-queries" class="level3" data-number="5.3">
<h3 data-number="5.3" class="anchored" data-anchor-id="parameterised-queries"><span class="header-section-number">5.3</span> Parameterised queries</h3>
<p>neo2R supports <strong>named parameters</strong>, keeping queries safe from injection and easy to reuse:</p>
<div class="cell">
<pre>## Find all co-stars of a given actor
cypher(
  graph,
  &quot;
  MATCH (a:Person {name: $actor})-[:ACTED_IN]-&gt;(m:Movie)&lt;-[:ACTED_IN]-(co:Person)
  RETURN DISTINCT co.name AS co_star, m.title AS shared_movie
  ORDER BY co_star
  &quot;,
  parameters = list(actor = &quot;Tom Hanks&quot;)
) |&gt;
  as_tibble()</pre>
<div class="cell-output cell-output-stdout">
<pre># A tibble: 114 × 2
   co_star            shared_movie        
   &lt;chr&gt;              &lt;chr&gt;               
 1 Adrian Zmed        Bachelor Party      
 2 Alexander Godunov  Money Pit, The      
 3 Amy Adams          Charlie Wilson's War
 4 Annie Rose Buckley Saving Mr. Banks    
 5 Audrey Tautou      Da Vinci Code, The  
 6 Ayelet Zurer       Angels & Demons     
 7 Barkhad Abdi       Captain Phillips    
 8 Barkhad Abdirahman Captain Phillips    
 9 Barry Pepper       Saving Private Ryan 
10 Bill Paxton        Apollo 13           
# &#x2139; 104 more rows</pre>
</div>
</div>
<hr>
</section>
</section>
<section id="network-visualisation-with-visnetwork" class="level2" data-number="6">
<h2 data-number="6" class="anchored" data-anchor-id="network-visualisation-with-visnetwork"><span class="header-section-number">6</span> Network visualisation with visNetwork</h2>
<p>The real power of a graph database is visible when you <em>draw</em> the graph. Let’s pull Tom Hanks’s ego network, everyone he has acted alongside, and render it with <strong>visNetwork</strong>.</p>
<section id="step-1-fetch-nodes-and-edges" class="level3" data-number="6.1">
<h3 data-number="6.1" class="anchored" data-anchor-id="step-1-fetch-nodes-and-edges"><span class="header-section-number">6.1</span> Step 1 — Fetch nodes and edges</h3>
<div class="cell">
<pre>## Tom Hanks, his movies, and his co-stars
hub &lt;- &quot;Tom Hanks&quot;
nodes_raw &lt;- cypher(
  graph,
  &quot;
  MATCH (hub:Person {name: $hub})-[hr:ACTED_IN]-&gt;(m:Movie)
  &lt;-[cr:ACTED_IN]-(co:Person)
  RETURN hub.name AS hub, hr.role AS hub_role,
  m.title AS movie, m.year AS year,
  co.name AS co, cr.role AS co_role
  &quot;,
  parameters = list(hub = hub)
) |&gt;
  as_tibble()</pre>
</div>
</section>
<section id="step-2-shape-data-for-visnetwork" class="level3" data-number="6.2">
<h3 data-number="6.2" class="anchored" data-anchor-id="step-2-shape-data-for-visnetwork"><span class="header-section-number">6.2</span> Step 2 — Shape data for visNetwork</h3>
<p>visNetwork expects two data frames: <code>nodes</code> (with columns <code>id</code>, <code>label</code>, <code>group</code>, …) and <code>edges</code> (with columns <code>from</code>, <code>to</code>, …).</p>
<div class="cell">
<pre>nodes &lt;- bind_rows(
  nodes_raw |&gt;
    distinct(
      id = hub,
      group = &quot;Hub&quot;
    ),
  nodes_raw |&gt;
    distinct(
      id = co,
      group = &quot;Co-star&quot;
    ),
  nodes_raw |&gt;
    distinct(
      id = movie,
      group = &quot;Movie&quot;,
      year
    )
) |&gt;
  distinct() |&gt;
  mutate(
    title = sprintf(
      '&lt;b&gt;%s&lt;/b&gt;: %s%s',
      group,
      id,
      ifelse(!is.na(year), sprintf(&quot;(%s)&quot;, year), &quot;&quot;)
    ),
    shape = ifelse(group == &quot;Movie&quot;, &quot;dot&quot;, &quot;star&quot;),
    size = ifelse(group == &quot;Hub&quot;, 30, 18)
  ) |&gt; 
    arrange(id)

edges &lt;- bind_rows(
  nodes_raw |&gt;
    distinct(
      from = hub,
      to = movie,
      role = hub_role
    ),
  nodes_raw |&gt;
    distinct(
      from = co,
      to = movie,
      role = co_role
    )
) |&gt;
  mutate(
    title = sprintf('&lt;b&gt;Role&lt;/b&gt;: %s', role),
    arrows = &quot;to&quot;
  )</pre>
</div>
</section>
<section id="step-3-draw-the-network" class="level3" data-number="6.3">
<h3 data-number="6.3" class="anchored" data-anchor-id="step-3-draw-the-network"><span class="header-section-number">6.3</span> Step 3 — Draw the network</h3>
<div class="cell">
<pre>visNetwork(nodes, edges) |&gt;
  visGroups(
    groupname = &quot;hub&quot;,
    color = list(
      background = &quot;#3B82F6&quot;,
      border = &quot;#1D4ED8&quot;,
      highlight = &quot;#93C5FD&quot;
    )
  ) |&gt;
  visGroups(
    groupname = &quot;movie&quot;,
    color = list(
      background = &quot;#F97316&quot;,
      border = &quot;#C2410C&quot;,
      highlight = &quot;#FED7AA&quot;
    ),
    shape = &quot;square&quot;
  ) |&gt;
  visGroups(
    groupname = &quot;costar&quot;,
    color = list(
      background = &quot;#6B7280&quot;,
      border = &quot;#374151&quot;,
      highlight = &quot;#D1D5DB&quot;
    )
  ) |&gt;
  visEdges(
    color = list(color = &quot;#CBD5E1&quot;, highlight = &quot;#3B82F6&quot;),
    width = 1.5
  ) |&gt;
  visOptions(
    highlightNearest = list(enabled = TRUE, degree = 1, hover = TRUE),
    nodesIdSelection = TRUE
  ) |&gt;
  visLayout(randomSeed = 42) |&gt;
  visPhysics(
    solver = &quot;forceAtlas2Based&quot;,
    forceAtlas2Based = list(
      gravitationalConstant = -60,
      springLength = 120,
      springConstant = 0.04
    )
  ) |&gt;
  visLegend(position = &quot;right&quot;, main = &quot;Node type&quot;)</pre>
<div class="cell-output-display">
<div class="visNetwork html-widget html-fill-item" id="htmlwidget-6a800980bfb41b9a5d8f" style="width:100%;height:464px;"></div>
<script type="application/json" data-for="htmlwidget-6a800980bfb41b9a5d8f">{"x":{"nodes":{"id":["'burbs, The","Adrian Zmed","Alexander Godunov","Amy Adams","Angels & Demons","Annie Rose Buckley","Apollo 13","Audrey Tautou","Ayelet Zurer","Bachelor Party","Barkhad Abdi","Barkhad Abdirahman","Barry Pepper","Big","Bill Paxton","Bonfire of the Vanities","Bonnie Hunt","Bruce Dern","Bruce Willis","Buzz Kilman","Captain Phillips","Carl Weathers","Carrie Fisher","Catch Me If You Can","Catherine Keener","Catherine Zeta-Jones","Charles Durning","Charlie Wilson's War","Chi McBride","Christopher Plummer","Christopher Walken","Cloud Atlas","Colin Farrell","Craig T. Nelson","Da Vinci Code, The","Dabney Coleman","Dan Aykroyd","Daryl Hannah","David Andrews","David Morse","Denzel Washington","Don Rickles","Dragnet","Eddie Deezen","Edward Burns","Elizabeth Perkins","Emma Thompson","Eugene Levy","Eva Marie Saint","Ewan McGregor","Extremely Loud and Incredibly Close","Forrest Gump","From the Earth to the Moon","Gary Sinise","Geena Davis","George Grizzard","Green Mile, The","Greg Kinnear","Halle Berry","Harry Morgan","Hector Elizondo","Hugo Weaving","Ian McKellen","Irma P. Hall","J.K. Simmons","Jackie Gleason","Jean Reno","Jim Broadbent","Jim Varney","Joan Cusack","Joe Versus the Volcano","John Candy","John Goodman","John Heard","Julia Roberts","Kelsey Grammer","Kevin Bacon","Kim Cattrall","Ladykillers, The","Lane Smith","Larry Crowne","League of Their Own, A","Leonardo DiCaprio","Leslie Zemeckis","Lloyd Bridges","Lori Petty","Lori Singer","Madonna","Man with One Red Shoe, The","Mare Winningham","Mark Rydell","Marlon Wayans","Martin Sheen","Maureen Stapleton","Meg Ryan","Melanie Griffith","Michael Clarke Duncan","Michael Conner Humphreys","Money Pit, The","Ned Beatty","Nick Searcy","Nona Gaye","Nothing in Common","Parker Posey","Philadelphia","Philip Seymour Hoffman","Polar Express, The","Punchline","Randall Park","Reginald VelJohnson","Rick Ducommun","Rita Wilson","Robert Loggia","Robert Stack","Roberta Maxwell","Robin Wright","Ross Malinger","Roxana Ortega","Sally Field","Sandra Bullock","Sarah Mahoney","Saving Mr. Banks","Saving Private Ryan","Shelley Long","Sleepless in Seattle","Splash","Stanley Tucci","Stellan Skarsgård","Tawny Kitaen","Terminal, The","Thomas Horn","Tim Allen","Tim Thomerson","Tom Hanks","Tom Sizemore","Toy Story","Toy Story 2","Toy Story 3","Toy Story of Terror","Turner & Hooch","Victor Garber","Volunteers","You've Got Mail","Zoe Caldwell"],"group":["Movie","Co-star","Co-star","Co-star","Movie","Co-star","Movie","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Movie","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Movie","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Movie","Co-star","Movie","Movie","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Co-star","Movie","Movie","Co-star","Movie","Movie","Co-star","Co-star","Co-star","Movie","Co-star","Co-star","Co-star","Hub","Co-star","Movie","Movie","Movie","Movie","Movie","Co-star","Movie","Movie","Co-star"],"year":[1989,null,null,null,2009,null,1995,null,null,1984,null,null,null,1988,null,1990,null,null,null,null,2013,null,null,2002,null,null,null,2007,null,null,null,2012,null,null,2006,null,null,null,null,null,null,null,1987,null,null,null,null,null,null,null,2011,1994,1998,null,null,null,1999,null,null,null,null,null,null,null,null,null,null,null,null,null,1990,null,null,null,null,null,null,null,2004,null,2011,1992,null,null,null,null,null,null,1985,null,null,null,null,null,null,null,null,null,1986,null,null,null,1986,null,1993,null,2004,1988,null,null,null,null,null,null,null,null,null,null,null,null,null,2013,1998,null,1993,1984,null,null,null,2004,null,null,null,null,null,1995,1999,2010,2013,1989,null,1985,1998,null],"title":["<b>Movie<\/b>: 'burbs, The(1989)","<b>Co-star<\/b>: Adrian Zmed","<b>Co-star<\/b>: Alexander Godunov","<b>Co-star<\/b>: Amy Adams","<b>Movie<\/b>: Angels & Demons(2009)","<b>Co-star<\/b>: Annie Rose Buckley","<b>Movie<\/b>: Apollo 13(1995)","<b>Co-star<\/b>: Audrey Tautou","<b>Co-star<\/b>: Ayelet Zurer","<b>Movie<\/b>: Bachelor Party(1984)","<b>Co-star<\/b>: Barkhad Abdi","<b>Co-star<\/b>: Barkhad Abdirahman","<b>Co-star<\/b>: Barry Pepper","<b>Movie<\/b>: Big(1988)","<b>Co-star<\/b>: Bill Paxton","<b>Movie<\/b>: Bonfire of the Vanities(1990)","<b>Co-star<\/b>: Bonnie Hunt","<b>Co-star<\/b>: Bruce Dern","<b>Co-star<\/b>: Bruce Willis","<b>Co-star<\/b>: Buzz Kilman","<b>Movie<\/b>: Captain Phillips(2013)","<b>Co-star<\/b>: Carl Weathers","<b>Co-star<\/b>: Carrie Fisher","<b>Movie<\/b>: Catch Me If You Can(2002)","<b>Co-star<\/b>: Catherine Keener","<b>Co-star<\/b>: Catherine Zeta-Jones","<b>Co-star<\/b>: Charles Durning","<b>Movie<\/b>: Charlie Wilson's War(2007)","<b>Co-star<\/b>: Chi McBride","<b>Co-star<\/b>: Christopher Plummer","<b>Co-star<\/b>: Christopher Walken","<b>Movie<\/b>: Cloud Atlas(2012)","<b>Co-star<\/b>: Colin Farrell","<b>Co-star<\/b>: Craig T. Nelson","<b>Movie<\/b>: Da Vinci Code, The(2006)","<b>Co-star<\/b>: Dabney Coleman","<b>Co-star<\/b>: Dan Aykroyd","<b>Co-star<\/b>: Daryl Hannah","<b>Co-star<\/b>: David Andrews","<b>Co-star<\/b>: David Morse","<b>Co-star<\/b>: Denzel Washington","<b>Co-star<\/b>: Don Rickles","<b>Movie<\/b>: Dragnet(1987)","<b>Co-star<\/b>: Eddie Deezen","<b>Co-star<\/b>: Edward Burns","<b>Co-star<\/b>: Elizabeth Perkins","<b>Co-star<\/b>: Emma Thompson","<b>Co-star<\/b>: Eugene Levy","<b>Co-star<\/b>: Eva Marie Saint","<b>Co-star<\/b>: Ewan McGregor","<b>Movie<\/b>: Extremely Loud and Incredibly Close(2011)","<b>Movie<\/b>: Forrest Gump(1994)","<b>Movie<\/b>: From the Earth to the Moon(1998)","<b>Co-star<\/b>: Gary Sinise","<b>Co-star<\/b>: Geena Davis","<b>Co-star<\/b>: George Grizzard","<b>Movie<\/b>: Green Mile, The(1999)","<b>Co-star<\/b>: Greg Kinnear","<b>Co-star<\/b>: Halle Berry","<b>Co-star<\/b>: Harry Morgan","<b>Co-star<\/b>: Hector Elizondo","<b>Co-star<\/b>: Hugo Weaving","<b>Co-star<\/b>: Ian McKellen","<b>Co-star<\/b>: Irma P. Hall","<b>Co-star<\/b>: J.K. Simmons","<b>Co-star<\/b>: Jackie Gleason","<b>Co-star<\/b>: Jean Reno","<b>Co-star<\/b>: Jim Broadbent","<b>Co-star<\/b>: Jim Varney","<b>Co-star<\/b>: Joan Cusack","<b>Movie<\/b>: Joe Versus the Volcano(1990)","<b>Co-star<\/b>: John Candy","<b>Co-star<\/b>: John Goodman","<b>Co-star<\/b>: John Heard","<b>Co-star<\/b>: Julia Roberts","<b>Co-star<\/b>: Kelsey Grammer","<b>Co-star<\/b>: Kevin Bacon","<b>Co-star<\/b>: Kim Cattrall","<b>Movie<\/b>: Ladykillers, The(2004)","<b>Co-star<\/b>: Lane Smith","<b>Movie<\/b>: Larry Crowne(2011)","<b>Movie<\/b>: League of Their Own, A(1992)","<b>Co-star<\/b>: Leonardo DiCaprio","<b>Co-star<\/b>: Leslie Zemeckis","<b>Co-star<\/b>: Lloyd Bridges","<b>Co-star<\/b>: Lori Petty","<b>Co-star<\/b>: Lori Singer","<b>Co-star<\/b>: Madonna","<b>Movie<\/b>: Man with One Red Shoe, The(1985)","<b>Co-star<\/b>: Mare Winningham","<b>Co-star<\/b>: Mark Rydell","<b>Co-star<\/b>: Marlon Wayans","<b>Co-star<\/b>: Martin Sheen","<b>Co-star<\/b>: Maureen Stapleton","<b>Co-star<\/b>: Meg Ryan","<b>Co-star<\/b>: Melanie Griffith","<b>Co-star<\/b>: Michael Clarke Duncan","<b>Co-star<\/b>: Michael Conner Humphreys","<b>Movie<\/b>: Money Pit, The(1986)","<b>Co-star<\/b>: Ned Beatty","<b>Co-star<\/b>: Nick Searcy","<b>Co-star<\/b>: Nona Gaye","<b>Movie<\/b>: Nothing in Common(1986)","<b>Co-star<\/b>: Parker Posey","<b>Movie<\/b>: Philadelphia(1993)","<b>Co-star<\/b>: Philip Seymour Hoffman","<b>Movie<\/b>: Polar Express, The(2004)","<b>Movie<\/b>: Punchline(1988)","<b>Co-star<\/b>: Randall Park","<b>Co-star<\/b>: Reginald VelJohnson","<b>Co-star<\/b>: Rick Ducommun","<b>Co-star<\/b>: Rita Wilson","<b>Co-star<\/b>: Robert Loggia","<b>Co-star<\/b>: Robert Stack","<b>Co-star<\/b>: Roberta Maxwell","<b>Co-star<\/b>: Robin Wright","<b>Co-star<\/b>: Ross Malinger","<b>Co-star<\/b>: Roxana Ortega","<b>Co-star<\/b>: Sally Field","<b>Co-star<\/b>: Sandra Bullock","<b>Co-star<\/b>: Sarah Mahoney","<b>Movie<\/b>: Saving Mr. Banks(2013)","<b>Movie<\/b>: Saving Private Ryan(1998)","<b>Co-star<\/b>: Shelley Long","<b>Movie<\/b>: Sleepless in Seattle(1993)","<b>Movie<\/b>: Splash(1984)","<b>Co-star<\/b>: Stanley Tucci","<b>Co-star<\/b>: Stellan Skarsgård","<b>Co-star<\/b>: Tawny Kitaen","<b>Movie<\/b>: Terminal, The(2004)","<b>Co-star<\/b>: Thomas Horn","<b>Co-star<\/b>: Tim Allen","<b>Co-star<\/b>: Tim Thomerson","<b>Hub<\/b>: Tom Hanks","<b>Co-star<\/b>: Tom Sizemore","<b>Movie<\/b>: Toy Story(1995)","<b>Movie<\/b>: Toy Story 2(1999)","<b>Movie<\/b>: Toy Story 3(2010)","<b>Movie<\/b>: Toy Story of Terror(2013)","<b>Movie<\/b>: Turner & Hooch(1989)","<b>Co-star<\/b>: Victor Garber","<b>Movie<\/b>: Volunteers(1985)","<b>Movie<\/b>: You've Got Mail(1998)","<b>Co-star<\/b>: Zoe Caldwell"],"shape":["dot","star","star","star","dot","star","dot","star","star","dot","star","star","star","dot","star","dot","star","star","star","star","dot","star","star","dot","star","star","star","dot","star","star","star","dot","star","star","dot","star","star","star","star","star","star","star","dot","star","star","star","star","star","star","star","dot","dot","dot","star","star","star","dot","star","star","star","star","star","star","star","star","star","star","star","star","star","dot","star","star","star","star","star","star","star","dot","star","dot","dot","star","star","star","star","star","star","dot","star","star","star","star","star","star","star","star","star","dot","star","star","star","dot","star","dot","star","dot","dot","star","star","star","star","star","star","star","star","star","star","star","star","star","dot","dot","star","dot","dot","star","star","star","dot","star","star","star","star","star","dot","dot","dot","dot","dot","star","dot","dot","star"],"size":[18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,18,30,18,18,18,18,18,18,18,18,18,18],"label":["'burbs, The","Adrian Zmed","Alexander Godunov","Amy Adams","Angels & Demons","Annie Rose Buckley","Apollo 13","Audrey Tautou","Ayelet Zurer","Bachelor Party","Barkhad Abdi","Barkhad Abdirahman","Barry Pepper","Big","Bill Paxton","Bonfire of the Vanities","Bonnie Hunt","Bruce Dern","Bruce Willis","Buzz Kilman","Captain Phillips","Carl Weathers","Carrie Fisher","Catch Me If You Can","Catherine Keener","Catherine Zeta-Jones","Charles Durning","Charlie Wilson's War","Chi McBride","Christopher Plummer","Christopher Walken","Cloud Atlas","Colin Farrell","Craig T. Nelson","Da Vinci Code, The","Dabney Coleman","Dan Aykroyd","Daryl Hannah","David Andrews","David Morse","Denzel Washington","Don Rickles","Dragnet","Eddie Deezen","Edward Burns","Elizabeth Perkins","Emma Thompson","Eugene Levy","Eva Marie Saint","Ewan McGregor","Extremely Loud and Incredibly Close","Forrest Gump","From the Earth to the Moon","Gary Sinise","Geena Davis","George Grizzard","Green Mile, The","Greg Kinnear","Halle Berry","Harry Morgan","Hector Elizondo","Hugo Weaving","Ian McKellen","Irma P. Hall","J.K. Simmons","Jackie Gleason","Jean Reno","Jim Broadbent","Jim Varney","Joan Cusack","Joe Versus the Volcano","John Candy","John Goodman","John Heard","Julia Roberts","Kelsey Grammer","Kevin Bacon","Kim Cattrall","Ladykillers, The","Lane Smith","Larry Crowne","League of Their Own, A","Leonardo DiCaprio","Leslie Zemeckis","Lloyd Bridges","Lori Petty","Lori Singer","Madonna","Man with One Red Shoe, The","Mare Winningham","Mark Rydell","Marlon Wayans","Martin Sheen","Maureen Stapleton","Meg Ryan","Melanie Griffith","Michael Clarke Duncan","Michael Conner Humphreys","Money Pit, The","Ned Beatty","Nick Searcy","Nona Gaye","Nothing in Common","Parker Posey","Philadelphia","Philip Seymour Hoffman","Polar Express, The","Punchline","Randall Park","Reginald VelJohnson","Rick Ducommun","Rita Wilson","Robert Loggia","Robert Stack","Roberta Maxwell","Robin Wright","Ross Malinger","Roxana Ortega","Sally Field","Sandra Bullock","Sarah Mahoney","Saving Mr. Banks","Saving Private Ryan","Shelley Long","Sleepless in Seattle","Splash","Stanley Tucci","Stellan Skarsgård","Tawny Kitaen","Terminal, The","Thomas Horn","Tim Allen","Tim Thomerson","Tom Hanks","Tom Sizemore","Toy Story","Toy Story 2","Toy Story 3","Toy Story of Terror","Turner & Hooch","Victor Garber","Volunteers","You've Got Mail","Zoe Caldwell"]},"edges":{"from":["Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Tom Hanks","Sally Field","Mark Rydell","John Goodman","Martin Sheen","Leonardo DiCaprio","Christopher Walken","Dan Aykroyd","Harry Morgan","Christopher Plummer","Colin Farrell","Emma Thompson","Annie Rose Buckley","Tawny Kitaen","Adrian Zmed","George Grizzard","Tim Thomerson","Rita Wilson","John Candy","Dabney Coleman","Lori Singer","Charles Durning","Eugene Levy","John Candy","Daryl Hannah","John Heard","Elizabeth Perkins","Robert Loggia","Hector Elizondo","Eva Marie Saint","Jackie Gleason","Alexander Godunov","Shelley Long","Maureen Stapleton","Tim Allen","Joan Cusack","Carl Weathers","Barkhad Abdirahman","Catherine Keener","Barkhad Abdi","Sarah Mahoney","Roxana Ortega","Randall Park","Hugo Weaving","Jim Broadbent","Halle Berry","Stellan Skarsgård","Ayelet Zurer","Ewan McGregor","Thomas Horn","Zoe Caldwell","Sandra Bullock","Amy Adams","Philip Seymour Hoffman","Julia Roberts","Tim Allen","Ned Beatty","Joan Cusack","David Andrews","Lane Smith","Nick Searcy","Bonnie Hunt","Michael Clarke Duncan","David Morse","Tom Sizemore","Edward Burns","Barry Pepper","Kelsey Grammer","Tim Allen","Joan Cusack","Bill Paxton","Kevin Bacon","Gary Sinise","Jim Varney","Tim Allen","Don Rickles","Geena Davis","Lori Petty","Madonna","Robin Wright","Sally Field","Michael Conner Humphreys","Roberta Maxwell","Denzel Washington","Buzz Kilman","Victor Garber","Rita Wilson","Ross Malinger","Craig T. Nelson","Reginald VelJohnson","Mare Winningham","Rick Ducommun","Carrie Fisher","Bruce Dern","Meg Ryan","Robert Stack","Lloyd Bridges","Bruce Willis","Melanie Griffith","Kim Cattrall","Catherine Zeta-Jones","Chi McBride","Stanley Tucci","Audrey Tautou","Jean Reno","Ian McKellen","Eddie Deezen","Leslie Zemeckis","Nona Gaye","Irma P. Hall","Marlon Wayans","J.K. Simmons","Meg Ryan","Parker Posey","Greg Kinnear"],"to":["Punchline","Catch Me If You Can","Dragnet","Saving Mr. Banks","Bachelor Party","Volunteers","Man with One Red Shoe, The","Splash","Big","Nothing in Common","Money Pit, The","Toy Story of Terror","Captain Phillips","Larry Crowne","Cloud Atlas","Angels & Demons","Extremely Loud and Incredibly Close","Charlie Wilson's War","Toy Story 3","From the Earth to the Moon","Green Mile, The","Saving Private Ryan","Toy Story 2","Apollo 13","Toy Story","League of Their Own, A","Forrest Gump","Philadelphia","Sleepless in Seattle","Turner & Hooch","'burbs, The","Joe Versus the Volcano","Bonfire of the Vanities","Terminal, The","Da Vinci Code, The","Polar Express, The","Ladykillers, The","You've Got Mail","Punchline","Punchline","Punchline","Catch Me If You Can","Catch Me If You Can","Catch Me If You Can","Dragnet","Dragnet","Dragnet","Saving Mr. Banks","Saving Mr. Banks","Saving Mr. Banks","Bachelor Party","Bachelor Party","Bachelor Party","Volunteers","Volunteers","Volunteers","Man with One Red Shoe, The","Man with One Red Shoe, The","Man with One Red Shoe, The","Splash","Splash","Splash","Big","Big","Big","Nothing in Common","Nothing in Common","Nothing in Common","Money Pit, The","Money Pit, The","Money Pit, The","Toy Story of Terror","Toy Story of Terror","Toy Story of Terror","Captain Phillips","Captain Phillips","Captain Phillips","Larry Crowne","Larry Crowne","Larry Crowne","Cloud Atlas","Cloud Atlas","Cloud Atlas","Angels & Demons","Angels & Demons","Angels & Demons","Extremely Loud and Incredibly Close","Extremely Loud and Incredibly Close","Extremely Loud and Incredibly Close","Charlie Wilson's War","Charlie Wilson's War","Charlie Wilson's War","Toy Story 3","Toy Story 3","Toy Story 3","From the Earth to the Moon","From the Earth to the Moon","From the Earth to the Moon","Green Mile, The","Green Mile, The","Green Mile, The","Saving Private Ryan","Saving Private Ryan","Saving Private Ryan","Toy Story 2","Toy Story 2","Toy Story 2","Apollo 13","Apollo 13","Apollo 13","Toy Story","Toy Story","Toy Story","League of Their Own, A","League of Their Own, A","League of Their Own, A","Forrest Gump","Forrest Gump","Forrest Gump","Philadelphia","Philadelphia","Philadelphia","Sleepless in Seattle","Sleepless in Seattle","Sleepless in Seattle","Turner & Hooch","Turner & Hooch","Turner & Hooch","'burbs, The","'burbs, The","'burbs, The","Joe Versus the Volcano","Joe Versus the Volcano","Joe Versus the Volcano","Bonfire of the Vanities","Bonfire of the Vanities","Bonfire of the Vanities","Terminal, The","Terminal, The","Terminal, The","Da Vinci Code, The","Da Vinci Code, The","Da Vinci Code, The","Polar Express, The","Polar Express, The","Polar Express, The","Ladykillers, The","Ladykillers, The","Ladykillers, The","You've Got Mail","You've Got Mail","You've Got Mail"],"role":["Steven Gold","Carl Hanratty","Pep Streebeck","Walt Disney","Rick Gassko","Lawrence Whatley Bourne III","Richard Harlan Drew","Allen Bauer","Joshua \"Josh\" Baskin","David Basner","Walter Fielding, Jr.","Woody (Voice)","Captain Richard Phillips","Larry Crowne","Dr. Henry Goose / Hotel Manager / Isaac Sachs / Dermot Hoggins / Cavendish Look-a-Like Actor / Zachry","Robert Langdon","Thomas Schell","Charlie Wilson","Woody (voice)",null,"Paul Edgecomb","Captain John H. Miller","Woody (voice)","Jim Lovell","Woody (voice)","Jimmy Dugan - Manager","Forrest Gump","Andrew Beckett","Sam Baldwin","Scott Turner","Ray Peterson","Joe Banks","Sherman McCoy","Viktor Navorski","Robert Langdon","Hero Boy / Father / Conductor / Hobo / Scrooge / Santa Claus (voice)","Professor G.H. Dorr","Joe Fox","Lilah Krytsick","Romeo","John Krytsick","Roger Strong","Frank Abagnale Jr.","Frank Abagnale","Sgt. Joe Friday","Captain Gannon","Reverend Jonathan Whirley","Travers Robert Goff","P.L. Travers","Ginty","Debbie Thompson","Jay O'Neill","Ed Thompson","John Reynolds","Beth Wexler","Tom Tuttle","Cooper","Maddy","Ross","Walter Kornbluth","Freddie Bauer","Madison","Paul","Susan","MacMillan",null,"Lorraine Basner","Max Basner","Max Beissart","Anna Crowley","Estelle","Buzz Lightyear (Voice)","Jessie (Voice)","Combat Carl / Combat Carl Jr. (Voice)","Bilal","Andrea Phillips","Muse","Samantha","Alvarez","Trainee Wong","Haskell Moore / Tadeusz Kesselring / Bill Smoke / Nurse Noakes / Boardman Mephi / Old Georgie","Captain Molyneux / Vyvyan Ayrs / Timothy Cavendish / Korean Musician / Prescient 2","Native Woman / Jocasta Ayrs / Luisa Rey / Indian Party Guest / Ovid / Meronym","Commander Richter","Vittoria Vetra","Camerlengo Patrick McKenna","Oskar Schell","Oskar's Grandmother","Linda Schell","Bonnie Bach","Gust Avrakotos","Joanne Herring","Buzz Lightyear (voice)","Lotso (voice)","Jessie the Yodeling Cowgirl (voice)",null,null,null,"Jan Edgecomb","John Coffey","Brutus \"Brutal\" Howell","Technical Sergeant Michael Horvath","Private Richard Reiben","Private Daniel Jackson","Stinky Pete the Prospector (voice)","Buzz Lightyear (voice)","Jessie the Yodeling Cowgirl (voice)","Fred Haise","Jack Swigert","Ken Mattingly","Slinky Dog (voice)","Buzz Lightyear (voice)","Mr. Potato Head (voice)","Dottie Hinson - Catcher","Kit Keller - Pitcher","Mae Mordabito - Center Field","Jenny Curran","Mrs. Gump","Young Forrest Gump","Judge Tate","Joe Miller","Crutches","Greg","Suzy","Jonah Baldwin","Howard Hyde","Det. David Sutton","Emily Carson","Art Weingartner","Carol Peterson","Lt. Mark Rumsfield","DeDe/Angelica Graynamore/Patricia Graynamore","Dr. Ellison","Samuel Harvey Graynamore","Peter Fallow","Maria Ruskin","Judy McCoy","Amelia Warren","Mulroy","Frank Dixon","Sophie Neveu","Captain Bezu Fache","Sir Leigh Teabing","Know-It-All (voice)","Sister Sarah / Mother (voice)","Hero Girl (voice)","Marva Munson","Gawain MacSam","Garth Pancake","Kathleen Kelly","Patricia Eden","Frank Navasky"],"title":["<b>Role<\/b>: Steven Gold","<b>Role<\/b>: Carl Hanratty","<b>Role<\/b>: Pep Streebeck","<b>Role<\/b>: Walt Disney","<b>Role<\/b>: Rick Gassko","<b>Role<\/b>: Lawrence Whatley Bourne III","<b>Role<\/b>: Richard Harlan Drew","<b>Role<\/b>: Allen Bauer","<b>Role<\/b>: Joshua \"Josh\" Baskin","<b>Role<\/b>: David Basner","<b>Role<\/b>: Walter Fielding, Jr.","<b>Role<\/b>: Woody (Voice)","<b>Role<\/b>: Captain Richard Phillips","<b>Role<\/b>: Larry Crowne","<b>Role<\/b>: Dr. Henry Goose / Hotel Manager / Isaac Sachs / Dermot Hoggins / Cavendish Look-a-Like Actor / Zachry","<b>Role<\/b>: Robert Langdon","<b>Role<\/b>: Thomas Schell","<b>Role<\/b>: Charlie Wilson","<b>Role<\/b>: Woody (voice)","<b>Role<\/b>: NA","<b>Role<\/b>: Paul Edgecomb","<b>Role<\/b>: Captain John H. Miller","<b>Role<\/b>: Woody (voice)","<b>Role<\/b>: Jim Lovell","<b>Role<\/b>: Woody (voice)","<b>Role<\/b>: Jimmy Dugan - Manager","<b>Role<\/b>: Forrest Gump","<b>Role<\/b>: Andrew Beckett","<b>Role<\/b>: Sam Baldwin","<b>Role<\/b>: Scott Turner","<b>Role<\/b>: Ray Peterson","<b>Role<\/b>: Joe Banks","<b>Role<\/b>: Sherman McCoy","<b>Role<\/b>: Viktor Navorski","<b>Role<\/b>: Robert Langdon","<b>Role<\/b>: Hero Boy / Father / Conductor / Hobo / Scrooge / Santa Claus (voice)","<b>Role<\/b>: Professor G.H. Dorr","<b>Role<\/b>: Joe Fox","<b>Role<\/b>: Lilah Krytsick","<b>Role<\/b>: Romeo","<b>Role<\/b>: John Krytsick","<b>Role<\/b>: Roger Strong","<b>Role<\/b>: Frank Abagnale Jr.","<b>Role<\/b>: Frank Abagnale","<b>Role<\/b>: Sgt. Joe Friday","<b>Role<\/b>: Captain Gannon","<b>Role<\/b>: Reverend Jonathan Whirley","<b>Role<\/b>: Travers Robert Goff","<b>Role<\/b>: P.L. Travers","<b>Role<\/b>: Ginty","<b>Role<\/b>: Debbie Thompson","<b>Role<\/b>: Jay O'Neill","<b>Role<\/b>: Ed Thompson","<b>Role<\/b>: John Reynolds","<b>Role<\/b>: Beth Wexler","<b>Role<\/b>: Tom Tuttle","<b>Role<\/b>: Cooper","<b>Role<\/b>: Maddy","<b>Role<\/b>: Ross","<b>Role<\/b>: Walter Kornbluth","<b>Role<\/b>: Freddie Bauer","<b>Role<\/b>: Madison","<b>Role<\/b>: Paul","<b>Role<\/b>: Susan","<b>Role<\/b>: MacMillan","<b>Role<\/b>: NA","<b>Role<\/b>: Lorraine Basner","<b>Role<\/b>: Max Basner","<b>Role<\/b>: Max Beissart","<b>Role<\/b>: Anna Crowley","<b>Role<\/b>: Estelle","<b>Role<\/b>: Buzz Lightyear (Voice)","<b>Role<\/b>: Jessie (Voice)","<b>Role<\/b>: Combat Carl / Combat Carl Jr. (Voice)","<b>Role<\/b>: Bilal","<b>Role<\/b>: Andrea Phillips","<b>Role<\/b>: Muse","<b>Role<\/b>: Samantha","<b>Role<\/b>: Alvarez","<b>Role<\/b>: Trainee Wong","<b>Role<\/b>: Haskell Moore / Tadeusz Kesselring / Bill Smoke / Nurse Noakes / Boardman Mephi / Old Georgie","<b>Role<\/b>: Captain Molyneux / Vyvyan Ayrs / Timothy Cavendish / Korean Musician / Prescient 2","<b>Role<\/b>: Native Woman / Jocasta Ayrs / Luisa Rey / Indian Party Guest / Ovid / Meronym","<b>Role<\/b>: Commander Richter","<b>Role<\/b>: Vittoria Vetra","<b>Role<\/b>: Camerlengo Patrick McKenna","<b>Role<\/b>: Oskar Schell","<b>Role<\/b>: Oskar's Grandmother","<b>Role<\/b>: Linda Schell","<b>Role<\/b>: Bonnie Bach","<b>Role<\/b>: Gust Avrakotos","<b>Role<\/b>: Joanne Herring","<b>Role<\/b>: Buzz Lightyear (voice)","<b>Role<\/b>: Lotso (voice)","<b>Role<\/b>: Jessie the Yodeling Cowgirl (voice)","<b>Role<\/b>: NA","<b>Role<\/b>: NA","<b>Role<\/b>: NA","<b>Role<\/b>: Jan Edgecomb","<b>Role<\/b>: John Coffey","<b>Role<\/b>: Brutus \"Brutal\" Howell","<b>Role<\/b>: Technical Sergeant Michael Horvath","<b>Role<\/b>: Private Richard Reiben","<b>Role<\/b>: Private Daniel Jackson","<b>Role<\/b>: Stinky Pete the Prospector (voice)","<b>Role<\/b>: Buzz Lightyear (voice)","<b>Role<\/b>: Jessie the Yodeling Cowgirl (voice)","<b>Role<\/b>: Fred Haise","<b>Role<\/b>: Jack Swigert","<b>Role<\/b>: Ken Mattingly","<b>Role<\/b>: Slinky Dog (voice)","<b>Role<\/b>: Buzz Lightyear (voice)","<b>Role<\/b>: Mr. Potato Head (voice)","<b>Role<\/b>: Dottie Hinson - Catcher","<b>Role<\/b>: Kit Keller - Pitcher","<b>Role<\/b>: Mae Mordabito - Center Field","<b>Role<\/b>: Jenny Curran","<b>Role<\/b>: Mrs. Gump","<b>Role<\/b>: Young Forrest Gump","<b>Role<\/b>: Judge Tate","<b>Role<\/b>: Joe Miller","<b>Role<\/b>: Crutches","<b>Role<\/b>: Greg","<b>Role<\/b>: Suzy","<b>Role<\/b>: Jonah Baldwin","<b>Role<\/b>: Howard Hyde","<b>Role<\/b>: Det. David Sutton","<b>Role<\/b>: Emily Carson","<b>Role<\/b>: Art Weingartner","<b>Role<\/b>: Carol Peterson","<b>Role<\/b>: Lt. Mark Rumsfield","<b>Role<\/b>: DeDe/Angelica Graynamore/Patricia Graynamore","<b>Role<\/b>: Dr. Ellison","<b>Role<\/b>: Samuel Harvey Graynamore","<b>Role<\/b>: Peter Fallow","<b>Role<\/b>: Maria Ruskin","<b>Role<\/b>: Judy McCoy","<b>Role<\/b>: Amelia Warren","<b>Role<\/b>: Mulroy","<b>Role<\/b>: Frank Dixon","<b>Role<\/b>: Sophie Neveu","<b>Role<\/b>: Captain Bezu Fache","<b>Role<\/b>: Sir Leigh Teabing","<b>Role<\/b>: Know-It-All (voice)","<b>Role<\/b>: Sister Sarah / Mother (voice)","<b>Role<\/b>: Hero Girl (voice)","<b>Role<\/b>: Marva Munson","<b>Role<\/b>: Gawain MacSam","<b>Role<\/b>: Garth Pancake","<b>Role<\/b>: Kathleen Kelly","<b>Role<\/b>: Patricia Eden","<b>Role<\/b>: Frank Navasky"],"arrows":["to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to","to"]},"nodesToDataframe":true,"edgesToDataframe":true,"options":{"width":"100%","height":"100%","nodes":{"shape":"dot"},"manipulation":{"enabled":false},"groups":{"hub":{"color":{"background":"#3B82F6","border":"#1D4ED8","highlight":"#93C5FD"}},"movie":{"color":{"background":"#F97316","border":"#C2410C","highlight":"#FED7AA"},"shape":"square"},"useDefaultGroups":true,"costar":{"color":{"background":"#6B7280","border":"#374151","highlight":"#D1D5DB"}}},"edges":{"width":1.5,"color":{"color":"#CBD5E1","highlight":"#3B82F6"}},"interaction":{"hover":true,"zoomSpeed":1},"layout":{"randomSeed":42},"physics":{"solver":"forceAtlas2Based","forceAtlas2Based":{"gravitationalConstant":-60,"springLength":120,"springConstant":0.04}}},"groups":["Movie","Co-star","Hub"],"width":null,"height":null,"idselection":{"enabled":true,"style":"width: 150px; height: 26px","useLabels":true,"main":"Select by id"},"byselection":{"enabled":false,"style":"width: 150px; height: 26px","multiple":false,"hideColor":"rgba(200,200,200,0.5)","highlight":false},"main":null,"submain":null,"footer":null,"background":"rgba(0, 0, 0, 0)","tooltipStay":300,"tooltipStyle":"position: fixed;visibility:hidden;padding: 5px;white-space: nowrap;font-family: verdana;font-size:14px;font-color:#000000;background-color: #f5f4ed;-moz-border-radius: 3px;-webkit-border-radius: 3px;border-radius: 3px;border: 1px solid #808074;box-shadow: 3px 3px 10px rgba(0, 0, 0, 0.2);","highlight":{"enabled":true,"hoverNearest":true,"degree":1,"algorithm":"all","hideColor":"rgba(200,200,200,0.5)","labelOnly":true},"collapse":{"enabled":false,"fit":false,"resetHighlight":true,"clusterOptions":null,"keepCoord":true,"labelSuffix":"(cluster)"},"legend":{"width":0.2,"useGroups":true,"position":"right","ncol":1,"stepX":100,"stepY":100,"zoom":true,"main":{"text":"Node type","style":"font-family:Georgia, Times New Roman, Times, serif;font-weight:bold;font-size:14px;text-align:center;"}}},"evals":[],"jsHooks":[]}</script>
</div>
</div>
<p>Hover over any node to see its label. Use the <strong>Select by id</strong> dropdown or a node to highlight movies shared with Tom Hanks.</p>
</section>
</section>
<section id="conclusion" class="level2" data-number="7">
<h2 data-number="7" class="anchored" data-anchor-id="conclusion"><span class="header-section-number">7</span> Conclusion</h2>
<p>neo2R 3.0.0 removes the last friction point for R users who want to work with <strong>Neo4j Aura</strong>: a single <code>startGraph()</code> call now handles cloud and local instances uniformly, the httr2 backend gives reliable retries and clean error handling, and the Cypher query interface remains exactly as it was.</p>
<section id="further-reading" class="level3" data-number="7.1">
<h3 data-number="7.1" class="anchored" data-anchor-id="further-reading"><span class="header-section-number">7.1</span> Further reading</h3>
<ul>
<li><a href="https://cran.r-project.org/package=neo2R" rel="nofollow" target="_blank">neo2R on CRAN</a> — package documentation</li>
<li><a href="https://github.com/patzaw/neo2R" rel="nofollow" target="_blank">neo2R GitHub</a> — source, changelog, and issues</li>
<li><a href="https://console.neo4j.io/" rel="nofollow" target="_blank">Neo4j Aura console</a> — create your free instance</li>
<li><a href="https://neo4j.com/docs/cypher-manual/current/" rel="nofollow" target="_blank">Neo4j Cypher reference</a> — query language docs</li>
<li><a href="https://datastorm-open.github.io/visNetwork/" rel="nofollow" target="_blank">visNetwork documentation</a> — all chart options</li>
</ul>


</section>
</section>

 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://patzaw.github.io/posts/neo2R-Aura.html"> Patrice Godard</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/querying-neo4j-aura-from-r-with-neo2r/">Querying Neo4j Aura from R with neo2R</a>]]></content:encoded>
					
		
		<enclosure url="https://patzaw.github.io/posts/images/neo2R-Aura.png" length="0" type="image/png" />

		<post-id xmlns="com-wordpress:feed-additions:1">401303</post-id>	</item>
		<item>
		<title>Leaflet attribution</title>
		<link>https://www.r-bloggers.com/2026/05/leaflet-attribution/</link>
		
		<dc:creator><![CDATA[Michael]]></dc:creator>
		<pubDate>Mon, 18 May 2026 04:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://r.iresmi.net/posts/2026/leaflet_attribution/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; ">
<p>Leaflets – CC-BY-NC-ND by Steve Walser</p>
<p>Note for myself and others: how to remove the “Leaflet &#124; ” prefix in map attribution using the R package {leaflet}.</p>
<p>Note</p>
<p>It’s allowed. Masking the attribution is sometimes useful in certain circumstances, but generally please cite the software and data used…</p>
<p>According to some sources ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/leaflet-attribution/">Leaflet attribution</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://r.iresmi.net/posts/2026/leaflet_attribution/"> r.iresmi.net</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
 






<div class="quarto-figure quarto-figure-center">
<figure class="figure">
<p><a href="https://www.flickr.com/photos/bassbro/54876966918/" rel="nofollow" target="_blank"><img src="https://i2.wp.com/r.iresmi.net/posts/2026/leaflet_attribution/images/54876966918_065fda8cbb_c.jpg?w=578&#038;ssl=1" class="preview-image img-fluid figure-img" alt="A photo of autumn leaves" data-recalc-dims="1"></a></p>
<figcaption>Leaflets – CC-BY-NC-ND by Steve Walser</figcaption>
</figure>
</div>
<p>Note for myself and others: how to remove the “Leaflet | ” prefix in map attribution using the R package {leaflet}.</p>
<div class="callout callout-style-default callout-note callout-titled">
<div class="callout-header d-flex align-content-center">
<div class="callout-icon-container">
<i class="callout-icon"></i>
</div>
<div class="callout-title-container flex-fill">
Note
</div>
</div>
<div class="callout-body-container callout-body">
<p>It’s <a href="https://groups.google.com/g/leaflet-js/c/fA6M7fbchOs/m/JTNVhqdc7JcJ?pli=1" rel="nofollow" target="_blank">allowed</a>. Masking the attribution is sometimes useful in certain circumstances, but generally please cite the software and data used…</p>
</div>
</div>
<p>According to <a href="https://stackoverflow.com/questions/57092107/how-can-i-remove-attribution-in-leaflet-map-in-r/77265384" rel="nofollow" target="_blank">some sources</a> we could write <code>leaflet(options = leafletOptions(attributionPrefix = &quot;&quot;))</code> but it doesn’t work in my case.</p>
<p>So instead we can execute some javascript:</p>
<div class="cell">
<pre>library(leaflet)

leaflet() |&gt;
  addTiles(urlTemplate = &quot;&quot;, attribution = &quot;Only my data&quot;) |&gt; 
  htmlwidgets::onRender(&quot;function(el, x) {
    // Remove Leaflet attribution prefix
    this.attributionControl.setPrefix('');
    }&quot;)</pre>
<div id="fig-leaflet" class="cell-output-display quarto-float quarto-figure quarto-figure-center anchored">
<figure class="quarto-float quarto-float-fig figure">
<div aria-describedby="fig-leaflet-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
<div class="leaflet html-widget html-fill-item" id="htmlwidget-bf42d06680c921c885f8" style="width:100%;height:464px;"></div>
<script type="application/json" data-for="htmlwidget-bf42d06680c921c885f8">{"x":{"options":{"crs":{"crsClass":"L.CRS.EPSG3857","code":null,"proj4def":null,"projectedBounds":null,"options":{}}},"calls":[{"method":"addTiles","args":["",null,null,{"minZoom":0,"maxZoom":18,"tileSize":256,"subdomains":"abc","errorTileUrl":"","tms":false,"noWrap":false,"zoomOffset":0,"zoomReverse":false,"opacity":1,"zIndex":1,"detectRetina":false,"attribution":"Only my data"}]}]},"evals":[],"jsHooks":{"render":[{"code":"function(el, x, data) {\n  return (function(el, x) {\n    // Remove Leaflet attribution prefix\n    this.attributionControl.setPrefix('');\n    }).call(this.getMap(), el, x, data);\n}","data":null}]}}</script>
</div>
<figcaption class="quarto-float-caption-bottom quarto-float-caption quarto-float-fig" id="fig-leaflet-caption-0ceaefa1-69ba-4598-a22c-09a6ac19f8ca">
Figure 1: A blank leaflet map with a custom attribution
</figcaption>
</figure>
</div>
</div>


<!-- -->


 
<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://r.iresmi.net/posts/2026/leaflet_attribution/"> r.iresmi.net</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/leaflet-attribution/">Leaflet attribution</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401318</post-id>	</item>
		<item>
		<title>Five tips for managing your R-universe 🚀</title>
		<link>https://www.r-bloggers.com/2026/05/five-tips-for-managing-your-r-universe-%f0%9f%9a%80/</link>
		
		<dc:creator><![CDATA[R &#124; Dr Tom Palmer]]></dc:creator>
		<pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate>
				<category><![CDATA[R bloggers]]></category>
		<guid isPermaLink="false">https://remlapmot.github.io/post/2026/runiverse-tips/</guid>

					<description><![CDATA[<div style = "width:60%; display: inline-block; float:left; "> Introduction</p>
<p>rOpenSci’s<br />
R-universe system is an open source platform allowing users to create their own CRAN-like universe of R packages.<br />
It is absolutely fantastic. It is particularly useful in one area I research, Mendelian randomization (at ...</p></div>
<div style = "width: 40%; display: inline-block; float:right;"></div>
<div style="clear: both;"></div>
<strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/five-tips-for-managing-your-r-universe-%f0%9f%9a%80/">Five tips for managing your R-universe 🚀</a>]]></description>
										<content:encoded><![CDATA[<!-- 
<div style="min-height: 30px;">
[social4i size="small" align="align-left"]
</div>
-->

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 12px;">
[This article was first published on  <strong><a href="https://remlapmot.github.io/post/2026/runiverse-tips/"> R | Dr Tom Palmer</a></strong>, and kindly contributed to <a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers</a>].  (You can report issue about the content on this page <a href="https://www.r-bloggers.com/contact-us/">here</a>)
<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div>
<h2 id="introduction">Introduction</h2>
<p>
<a href="https://ropensci.org/" rel="nofollow" target="_blank">rOpenSci</a>’s 
<a href="https://r-universe.dev/" rel="nofollow" target="_blank">R-universe</a> system is an open source platform allowing users to create their own CRAN-like universe of R packages.</p>
<p>It is absolutely fantastic. It is particularly useful in one area I research, Mendelian randomization (at the interface of Epidemiology and Genetic Epidemiology), because a lot of the packages are GitHub/GitLab-only.</p>
<p>Therefore, I setup and maintain 
<a href="https://mrcieu.r-universe.dev/" rel="nofollow" target="_blank">https://mrcieu.r-universe.dev/</a> to include both packages from our MRCIEU GitHub organisation (from the MRC Integrative Epidemiology Unit at the University of Bristol, UK), and as many of the GitHub-only packages for Mendelian randomization I could find.</p>
<p>It is difficult to overstate how useful this is. For the first time, not only do researchers have a list of the Mendelian randomization packages in one place but they can install binaries &#8211; without having to go through the hassle &#8211; especially on (Ubuntu) Linux &#8211; of <code>remotes::install_github()</code>. Researchers can also see how often packages are updated and R-universe checks for changes in packages approximately every hour, keeping it always up to date.</p>
<p>This post gives five tips I have developed to help manage my R-universe.</p>
<h2 id="tip-1-referring-to-a-package-from-a-pull-request-instead-of-from-a-branch-on-a-fork">Tip 1: Referring to a package from a pull request instead of from a branch on a fork</h2>
<p>In the Mendelian randomization field many of these GitHub-only packages are not well written or abandoned once the PhD student/researcher leaves. Often when I add a package to our R-universe I find that their build fails, or they have <code>R CMD check</code> errors and warnings, or after several months their build fails because they are not maintained. I sometimes look into the failed builds and <code>check</code> problems. If it’s clear just a few fixes are required to rectify the situation I often open a pull request. Often that pull request is not responded to.</p>
<p>Previously, for such cases I would switch the source of the package entry in <em>packages.json</em> to be from the relevant branch on my fork. However, I have always felt a bit uneasy about this. I wondered if GitHub had a way to refer to the pull request branch without having to switch the repository. It turns out that it does. The format of pull request branch names is <code>refs/pull/{number}/head</code> where <code>{number}</code> is the number assigned once the PR is opened. Therefore, when I open a PR on a package I now add the <code>&quot;branch&quot;</code> field to the package entry in <em>packages.json</em> as follows.</p>
<pre>  {
    &quot;package&quot;: &quot;GWASBrewer&quot;,
    &quot;url&quot;: &quot;https://github.com/jean997/GWASBrewer&quot;,
    &quot;branch&quot;: &quot;refs/pull/18/head&quot;
  },
</pre>
<p>I switch back to the default branch if the PR is merged.</p>
<h2 id="tip-2-justfile-recipe-for-adding-a-package-to-packagesjson">Tip 2: Justfile recipe for adding a package to packages.json</h2>
<p>I regularly find that I need to add or remove a package. Manually editing the <em>packages.json</em> file is not hard, but I have found the following 
<a href="https://just.systems/" rel="nofollow" target="_blank">Justfile</a> (Just is like Make, but specifically designed for running commands and has a much friendlier syntax) recipes helpful for doing this quickly.</p>
<p>These recipes require 
<a href="https://docs.astral.sh/uv/" rel="nofollow" target="_blank">uv</a> and just to be installed and on your <code>PATH</code> (uv automatically installs the required version of Python and creates/destroys/manages any required virtual environments). To use them, copy them into a text file named <em>justfile</em> at the top level of your R-universe registry repository and follow the instructions.</p>
<p>This recipe adds a package to your <em>packages.json</em> in alphabetical order. It has one required argument and 3 optional arguments.</p>
<pre># add a package entry to packages.json in alphabetical order
[arg(&quot;branch&quot;, short=&quot;b&quot;)]
[arg(&quot;pkgname&quot;, short=&quot;p&quot;)]
[arg(&quot;subdir&quot;, short=&quot;s&quot;)]
add url pkgname=&quot;&quot; branch=&quot;&quot; subdir=&quot;&quot;:
    #!/usr/bin/env -S uv run --python 3.14 python3
    import json, re, sys
    url = &quot;{{ url }}&quot;
    if re.fullmatch(r'[^/]+/[^/]+', url):
        url = f&quot;https://github.com/{url}&quot;
    pkgname = &quot;{{ pkgname }}&quot; or url.rstrip(&quot;/&quot;).split(&quot;/&quot;)[-1]
    branch = &quot;{{ branch }}&quot;
    subdir = &quot;{{ subdir }}&quot;
    with open(&quot;packages.json&quot;) as f:
        packages = json.load(f)
    if any(p[&quot;package&quot;] == pkgname for p in packages):
        print(f&quot;Error: '{pkgname}' already exists in packages.json&quot;, file=sys.stderr)
        sys.exit(1)
    entry = {&quot;package&quot;: pkgname, &quot;url&quot;: url}
    if branch:
        entry[&quot;branch&quot;] = branch
    if subdir:
        entry[&quot;subdir&quot;] = subdir
    packages.append(entry)
    packages.sort(key=lambda p: p[&quot;package&quot;].lower())
    with open(&quot;packages.json&quot;, &quot;w&quot;) as f:
        json.dump(packages, f, indent=2)
        f.write(&quot;\n&quot;)
    print(f&quot;Added {pkgname}&quot;)
</pre>
<p>Where <code>url</code> is say <code>https://github.com/MRCIEU/TwoSampleMR</code>, except that for GitHub packages you can specify this as <code>MRCIEU/TwoSampleMR</code>.</p>
<p>To add a GitHub package whose name matches its repository name, simply run</p>
<pre>just add username/reponame
</pre>
<p>You can inspect the recipe’s arguments and options with</p>
<pre>just --usage add

Usage: just add [OPTIONS] url

Arguments:
  url

Options:
  -p pkgname [default: &quot;&quot;]
  -b branch [default: &quot;&quot;]
  -s subdir [default: &quot;&quot;]
</pre>
<p>The 3 optional arguments allow you to specify the package name (<code>-p pkgname</code>), branch (<code>-b branchname</code>), or subdirectory (<code>-s subdirectory</code>) the package is in. For example, to add a GitHub package whose package name does not match its repository name run</p>
<pre>just add username/reponame -p pkgname
</pre>
<h2 id="tip-3-justfile-recipe-for-removing-a-package-from-packagesjson">Tip 3: Justfile recipe for removing a package from packages.json</h2>
<p>This recipe removes a package from your <em>packages.json</em>.</p>
<pre># remove a package entry from packages.json
remove pkgname:
    #!/usr/bin/env -S uv run --python 3.14 python3
    import json, sys
    pkgname = &quot;{{ pkgname }}&quot;
    with open(&quot;packages.json&quot;) as f:
        packages = json.load(f)
    filtered = [p for p in packages if p[&quot;package&quot;] != pkgname]
    if len(filtered) == len(packages):
        print(f&quot;Error: '{pkgname}' not found in packages.json&quot;, file=sys.stderr)
        sys.exit(1)
    with open(&quot;packages.json&quot;, &quot;w&quot;) as f:
        json.dump(filtered, f, indent=2)
        f.write(&quot;\n&quot;)
    print(f&quot;Removed {pkgname}&quot;)
</pre>
<p>Run it with</p>
<pre>just remove pkgname
</pre>
<h2 id="tip-4-justfile-recipe-for-checking-packagesjson-is-valid">Tip 4: Justfile recipe for checking packages.json is valid</h2>
<p>When manually editing <em>packages.json</em> it is very easy to forget a comma or to miss a closing bracket or quotation mark. This recipe checks your JSON is valid.</p>
<pre># check packages.json
check:
    uv run --python 3.14 -m json.tool packages.json &gt; /dev/null && echo &quot;JSON check passed&quot;
</pre>
<p>Run it with</p>
<pre>just check
</pre>
<h2 id="tip-5-conveniently-view-a-packages-dependencies">Tip 5: Conveniently view a package’s dependencies</h2>
<p>Knowing a package’s full strong dependency list is useful — for example, when a breaking change somewhere in the chain causes unexpected build failures. While there are several ways to determine this in R, R-universe shows you the full list immediately.</p>
<p>Navigate to the R-universe page for the package you are interested in, say 
<a href="https://mrcieu.r-universe.dev/TwoSampleMR" rel="nofollow" target="_blank">https://mrcieu.r-universe.dev/TwoSampleMR</a> and click the dependencies pill.</p>
<img src="https://i0.wp.com/remlapmot.github.io/post/2026/runiverse-tips/img/twosamplemr-dependencies-hover.png?w=450&#038;ssl=1" alt="Screenshot of hovering mouse over dependencies pill on an R-universe package page." style="display: block; margin: auto;" data-recalc-dims="1">
<p>It expands showing the full dependency list.</p>
<img src="https://i2.wp.com/remlapmot.github.io/post/2026/runiverse-tips/img/twosamplemr-dependencies-clicked.png?w=450&#038;ssl=1" alt="Screenshot of expanding the dependencies pill on an R-universe package page." style="display: block; margin: auto;" data-recalc-dims="1">
<h2 id="summary">Summary</h2>
<p>In summary, I have shown five tips I find useful to manage a large R-universe.</p>

<div style="border: 1px solid; background: none repeat scroll 0 0 #EDEDED; margin: 1px; font-size: 13px;">
<div style="text-align: center;">To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://remlapmot.github.io/post/2026/runiverse-tips/"> R | Dr Tom Palmer</a></strong>.</div>
<hr />
<a href="https://www.r-bloggers.com/" rel="nofollow">R-bloggers.com</a> offers <strong><a href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers" rel="nofollow">daily e-mail updates</a></strong> about <a title="The R Project for Statistical Computing" href="https://www.r-project.org/" rel="nofollow">R</a> news and tutorials about <a title="R tutorials" href="https://www.r-bloggers.com/how-to-learn-r-2/" rel="nofollow">learning R</a> and many other topics. <a title="Data science jobs" href="https://www.r-users.com/" rel="nofollow">Click here if you're looking to post or find an R/data-science job</a>.

<hr>Want to share your content on R-bloggers?<a href="https://www.r-bloggers.com/add-your-blog/" rel="nofollow"> click here</a> if you have a blog, or <a href="http://r-posts.com/" rel="nofollow"> here</a> if you don't.
</div><strong>Continue reading</strong>: <a href="https://www.r-bloggers.com/2026/05/five-tips-for-managing-your-r-universe-%f0%9f%9a%80/">Five tips for managing your R-universe 🚀</a>]]></content:encoded>
					
		
		<enclosure url="" length="0" type="" />

		<post-id xmlns="com-wordpress:feed-additions:1">401290</post-id>	</item>
	</channel>
</rss>
